diff --git a/LICENSE b/LICENSE index 53c5ccf39a..60e86a1cc7 100644 --- a/LICENSE +++ b/LICENSE @@ -6,7 +6,7 @@ Licensed under the Apache License version 2.0 (the "License"); You may not use this software except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 -This software includes third-party software. +This software includes third-party and other platform software. Please see each individual software license for additional details. This software is distributed "as-is" without any warranties, conditions, or representations whether express or implied, including without limitation the implied warranties and conditions of merchantability, merchantable quality, fitness for a particular purpose, performance, durability, title, non-infringement, and those arising from statute or from custom or usage of trade or course of dealing. diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index f8f0f7904a..65e193dec6 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -290,7 +290,6 @@ void Agent::executeScript() { packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::AvatarBillboard, avatarHashMap.data(), "processAvatarBillboardPacket"); // register ourselves to the script engine _scriptEngine->registerGlobalObject("Agent", this); @@ -341,15 +340,12 @@ void Agent::setIsAvatar(bool isAvatar) { if (_isAvatar && !_avatarIdentityTimer) { // set up the avatar timers _avatarIdentityTimer = new QTimer(this); - _avatarBillboardTimer = new QTimer(this); // connect our slot connect(_avatarIdentityTimer, &QTimer::timeout, this, &Agent::sendAvatarIdentityPacket); - connect(_avatarBillboardTimer, &QTimer::timeout, this, &Agent::sendAvatarBillboardPacket); // start the timers _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); - _avatarBillboardTimer->start(AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS); } if (!_isAvatar) { @@ -359,12 +355,6 @@ void Agent::setIsAvatar(bool isAvatar) { delete _avatarIdentityTimer; _avatarIdentityTimer = nullptr; } - - if (_avatarBillboardTimer) { - _avatarBillboardTimer->stop(); - delete _avatarBillboardTimer; - _avatarBillboardTimer = nullptr; - } } } @@ -375,14 +365,6 @@ void Agent::sendAvatarIdentityPacket() { } } -void Agent::sendAvatarBillboardPacket() { - if (_isAvatar) { - auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>(); - scriptedAvatar->sendBillboardPacket(); - } -} - - void Agent::processAgentAvatarAndAudio(float deltaTime) { if (!_scriptEngine->isFinished() && _isAvatar) { auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>(); @@ -491,7 +473,7 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { } void Agent::aboutToFinish() { - setIsAvatar(false);// will stop timers for sending billboards and identity packets + setIsAvatar(false);// will stop timers for sending identity packets if (_scriptEngine) { _scriptEngine->stop(); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 2b0d22385d..63d4cfa4d6 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -82,7 +82,6 @@ private: void setAvatarSound(SharedSoundPointer avatarSound) { _avatarSound = avatarSound; } void sendAvatarIdentityPacket(); - void sendAvatarBillboardPacket(); QString _scriptContents; QTimer* _scriptRequestTimeout { nullptr }; @@ -92,7 +91,6 @@ private: int _numAvatarSoundSentBytes = 0; bool _isAvatar = false; QTimer* _avatarIdentityTimer = nullptr; - QTimer* _avatarBillboardTimer = nullptr; QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers; }; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cc17b36e7c..366fdc8592 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -349,19 +349,14 @@ public: }; #endif -enum CustomEventTypes { - Lambda = QEvent::User + 1, - Paint = Lambda + 1, -}; - class LambdaEvent : public QEvent { std::function<void()> _fun; public: LambdaEvent(const std::function<void()> & fun) : - QEvent(static_cast<QEvent::Type>(Lambda)), _fun(fun) { + QEvent(static_cast<QEvent::Type>(Application::Lambda)), _fun(fun) { } LambdaEvent(std::function<void()> && fun) : - QEvent(static_cast<QEvent::Type>(Lambda)), _fun(fun) { + QEvent(static_cast<QEvent::Type>(Application::Lambda)), _fun(fun) { } void call() const { _fun(); } }; @@ -1062,18 +1057,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); - _idleTimer = new QTimer(this); - connect(_idleTimer, &QTimer::timeout, [=] { - idle(usecTimestampNow()); - }); - connect(this, &Application::beforeAboutToQuit, [=] { - disconnect(_idleTimer); - }); - // Setting the interval to zero forces this to get called whenever there are no messages - // in the queue, which can be pretty damn frequent. Hence the idle function has a bunch - // of logic to abort early if it's being called too often. - _idleTimer->start(0); - // After all of the constructor is completed, then set firstRun to false. Setting::Handle<bool> firstRun{ Settings::firstRun, true }; firstRun.set(false); @@ -1161,10 +1144,16 @@ void Application::cleanupBeforeQuit() { getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) + QThreadPool::globalInstance()->clear(); + DependencyManager::get<ScriptEngines>()->saveScripts(); DependencyManager::get<ScriptEngines>()->shutdownScripting(); // stop all currently running global scripts DependencyManager::destroy<ScriptEngines>(); + // 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(); @@ -1440,23 +1429,15 @@ void Application::initializeUi() { }); } - void Application::paintGL() { - updateHeartbeat(); - // Some plugins process message events, potentially leading to - // re-entering a paint event. don't allow further processing if this - // happens - if (_inPaint) { + // Some plugins process message events, allowing paintGL to be called reentrantly. + if (_inPaint || _aboutToQuit) { return; } - _inPaint = true; - Finally clearFlagLambda([this] { _inPaint = false; }); - // paintGL uses a queued connection, so we can get messages from the queue even after we've quit - // and the plugins have shutdown - if (_aboutToQuit) { - return; - } + _inPaint = true; + Finally clearFlag([this] { _inPaint = false; }); + _frameCount++; _frameCounter.increment(); @@ -1814,13 +1795,30 @@ bool Application::event(QEvent* event) { return false; } - if ((int)event->type() == (int)Lambda) { - static_cast<LambdaEvent*>(event)->call(); + static bool justPresented = false; + if ((int)event->type() == (int)Present) { + if (justPresented) { + justPresented = false; + + // If presentation is hogging the main thread, repost as low priority to avoid hanging the GUI. + // This has the effect of allowing presentation to exceed the paint budget by X times and + // only dropping every (1/X) frames, instead of every ceil(X) frames. + // (e.g. at a 60FPS target, painting for 17us would fall to 58.82FPS instead of 30FPS). + removePostedEvents(this, Present); + postEvent(this, new QEvent(static_cast<QEvent::Type>(Present)), Qt::LowEventPriority); + return true; + } + + idle(); + return true; + } else if ((int)event->type() == (int)Paint) { + justPresented = true; + paintGL(); return true; } - if ((int)event->type() == (int)Paint) { - paintGL(); + if ((int)event->type() == (int)Lambda) { + static_cast<LambdaEvent*>(event)->call(); return true; } @@ -2598,72 +2596,63 @@ bool Application::acceptSnapshot(const QString& urlString) { static uint32_t _renderedFrameIndex { INVALID_FRAME }; -void Application::idle(uint64_t now) { - // NOTICE NOTICE NOTICE NOTICE - // Do not insert new code between here and the PROFILE_RANGE declaration - // unless you know exactly what you're doing. This idle function can be - // called thousands per second or more, so any additional work that's done - // here will have a serious impact on CPU usage. Only add code after all - // the thottling logic, i.e. after PROFILE_RANGE - // NOTICE NOTICE NOTICE NOTICE - updateHeartbeat(); - - if (_aboutToQuit || _inPaint) { - return; // bail early, nothing to do here. - } - - auto displayPlugin = getActiveDisplayPlugin(); - // depending on whether we're throttling or not. - // Once rendering is off on another thread we should be able to have Application::idle run at start(0) in - // perpetuity and not expect events to get backed up. - bool isThrottled = displayPlugin->isThrottled(); - // Only run simulation code if more than the targetFramePeriod have passed since last time we ran - // This attempts to lock the simulation at 60 updates per second, regardless of framerate - float timeSinceLastUpdateUs = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_USEC; - float secondsSinceLastUpdate = timeSinceLastUpdateUs / USECS_PER_SECOND; - - if (isThrottled && (timeSinceLastUpdateUs / USECS_PER_MSEC) < THROTTLED_SIM_FRAME_PERIOD_MS) { - // Throttling both rendering and idle - return; // bail early, we're throttled and not enough time has elapsed - } - - auto presentCount = displayPlugin->presentCount(); - if (presentCount < _renderedFrameIndex) { - _renderedFrameIndex = INVALID_FRAME; - } - - // Don't saturate the main thread with rendering and simulation, - // unless display plugin has increased by at least one frame - if (_renderedFrameIndex == INVALID_FRAME || presentCount > _renderedFrameIndex) { - // Record what present frame we're on - _renderedFrameIndex = presentCount; - - // request a paint, get to it as soon as possible: high priority - postEvent(this, new QEvent(static_cast<QEvent::Type>(Paint)), Qt::HighEventPriority); - } else { - // there's no use in simulating or rendering faster then the present rate. +void Application::idle() { + // idle is called on a queued connection, so make sure we should be here. + if (_inPaint || _aboutToQuit) { return; } - // NOTICE NOTICE NOTICE NOTICE - // do NOT add new code above this line unless you want it to be executed potentially - // thousands of times per second - // NOTICE NOTICE NOTICE NOTICE + auto displayPlugin = getActiveDisplayPlugin(); - PROFILE_RANGE(__FUNCTION__); +#ifdef DEBUG_PAINT_DELAY + static uint64_t paintDelaySamples{ 0 }; + static uint64_t paintDelayUsecs{ 0 }; + + paintDelayUsecs += displayPlugin->getPaintDelayUsecs(); + + static const int PAINT_DELAY_THROTTLE = 1000; + if (++paintDelaySamples % PAINT_DELAY_THROTTLE == 0) { + qCDebug(interfaceapp).nospace() << + "Paint delay (" << paintDelaySamples << " samples): " << + (float)paintDelaySamples / paintDelayUsecs << "us"; + } +#endif + + float msecondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_USEC / USECS_PER_MSEC; + + // Throttle if requested + if (displayPlugin->isThrottled() && (msecondsSinceLastUpdate < THROTTLED_SIM_FRAME_PERIOD_MS)) { + return; + } + + // Sync up the _renderedFrameIndex + _renderedFrameIndex = displayPlugin->presentCount(); + + // Request a paint ASAP + postEvent(this, new QEvent(static_cast<QEvent::Type>(Paint)), Qt::HighEventPriority + 1); + + // Update the deadlock watchdog + updateHeartbeat(); + + auto offscreenUi = DependencyManager::get<OffscreenUi>(); // 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; - auto offscreenUi = DependencyManager::get<OffscreenUi>(); connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); _overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays)); + } else { + // FIXME: AvatarInputs are positioned incorrectly if instantiated before the first paint + AvatarInputs::getInstance()->update(); } + PROFILE_RANGE(__FUNCTION__); + + float secondsSinceLastUpdate = msecondsSinceLastUpdate / MSECS_PER_SECOND; + // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. - auto offscreenUi = DependencyManager::get<OffscreenUi>(); if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) { _keyboardMouseDevice->pluginFocusOutEvent(); _keyboardDeviceHasFocus = false; @@ -2679,7 +2668,6 @@ void Application::idle(uint64_t now) { checkChangeCursor(); Stats::getInstance()->updateStats(); - AvatarInputs::getInstance()->update(); _simCounter.increment(); diff --git a/interface/src/Application.h b/interface/src/Application.h index a4bbdf4326..558190c8d1 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -33,6 +33,7 @@ #include <PhysicalEntitySimulation.h> #include <PhysicsEngine.h> #include <plugins/Forward.h> +#include <plugins/DisplayPlugin.h> #include <ScriptEngine.h> #include <ShapeManager.h> #include <SimpleMovingAverage.h> @@ -93,6 +94,12 @@ class Application : public QApplication, public AbstractViewStateInterface, publ friend class PluginContainerProxy; public: + enum Event { + Present = DisplayPlugin::Present, + Paint = Present + 1, + Lambda = Paint + 1 + }; + // FIXME? Empty methods, do we still need them? static void initPlugins(); static void shutdownPlugins(); @@ -281,7 +288,6 @@ public slots: private slots: void showDesktop(); void clearDomainOctreeDetails(); - void idle(uint64_t now); void aboutToQuit(); void resettingDomain(); @@ -320,6 +326,7 @@ private: void cleanupBeforeQuit(); + void idle(); void update(float deltaTime); // Various helper functions called during update() @@ -497,7 +504,6 @@ private: int _avatarAttachmentRequest = 0; bool _settingsLoaded { false }; - QTimer* _idleTimer { nullptr }; bool _fakedMouseEvent { false }; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 41bcc0332a..ddadcb3909 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -73,7 +73,6 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::AvatarBillboard, this, "processAvatarBillboardPacket"); } AvatarManager::~AvatarManager() { diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 1179fbaa50..9ff7f6268f 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -36,15 +36,8 @@ #include <QtQuick/QQuickWindow> -Overlays::Overlays() : _nextOverlayID(1) { - connect(qApp, &Application::beforeAboutToQuit, [=] { - cleanupAllOverlays(); - }); -} - -Overlays::~Overlays() { -} - +Overlays::Overlays() : + _nextOverlayID(1) {} void Overlays::cleanupAllOverlays() { { diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 25ba00fdf0..f47f8de153 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -62,7 +62,6 @@ class Overlays : public QObject { public: Overlays(); - ~Overlays(); void init(); void update(float deltatime); @@ -73,6 +72,8 @@ public: Overlay::Pointer getOverlay(unsigned int id) const; OverlayPanel::Pointer getPanel(unsigned int id) const { return _panels[id]; } + void cleanupAllOverlays(); + public slots: /// adds an overlay with the specific properties unsigned int addOverlay(const QString& type, const QVariant& properties); @@ -145,7 +146,6 @@ signals: private: void cleanupOverlaysToDelete(); - void cleanupAllOverlays(); QMap<unsigned int, Overlay::Pointer> _overlaysHUD; QMap<unsigned int, Overlay::Pointer> _overlaysWorld; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 683a3062bb..b26cecbc9e 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -58,7 +58,6 @@ AvatarData::AvatarData() : _headData(NULL), _displayNameTargetAlpha(1.0f), _displayNameAlpha(1.0f), - _billboard(), _errorLogExpiry(0), _owningAvatarMixer(), _targetVelocity(0.0f), @@ -1006,13 +1005,6 @@ QByteArray AvatarData::identityByteArray() { return identityData; } -bool AvatarData::hasBillboardChangedAfterParsing(const QByteArray& data) { - if (data == _billboard) { - return false; - } - _billboard = data; - return true; -} void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { const QUrl& expanded = skeletonModelURL.isEmpty() ? AvatarData::defaultFullAvatarModelUrl() : skeletonModelURL; @@ -1110,33 +1102,6 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) { setAttachmentData(attachmentData); } -void AvatarData::setBillboard(const QByteArray& billboard) { - _billboard = billboard; - - qCDebug(avatars) << "Changing billboard for avatar."; -} - -void AvatarData::setBillboardFromURL(const QString &billboardURL) { - _billboardURL = billboardURL; - - - qCDebug(avatars) << "Changing billboard for avatar to PNG at" << qPrintable(billboardURL); - - QNetworkRequest billboardRequest; - billboardRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - billboardRequest.setUrl(QUrl(billboardURL)); - - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkReply* networkReply = networkAccessManager.get(billboardRequest); - connect(networkReply, SIGNAL(finished()), this, SLOT(setBillboardFromNetworkReply())); -} - -void AvatarData::setBillboardFromNetworkReply() { - QNetworkReply* networkReply = static_cast<QNetworkReply*>(sender()); - setBillboard(networkReply->readAll()); - networkReply->deleteLater(); -} - void AvatarData::setJointMappingsFromNetworkReply() { QNetworkReply* networkReply = static_cast<QNetworkReply*>(sender()); @@ -1213,21 +1178,6 @@ void AvatarData::sendIdentityPacket() { _avatarEntityDataLocallyEdited = false; } -void AvatarData::sendBillboardPacket() { - if (!_billboard.isEmpty()) { - auto nodeList = DependencyManager::get<NodeList>(); - - // This makes sure the billboard won't be too large to send. - // Once more protocol changes are done and we can send blocks of data we can support sending > MTU sized billboards. - if (_billboard.size() <= NLPacket::maxPayloadSize(PacketType::AvatarBillboard)) { - auto billboardPacket = NLPacket::create(PacketType::AvatarBillboard, _billboard.size()); - billboardPacket->write(_billboard); - - nodeList->broadcastToNodes(std::move(billboardPacket), NodeSet() << NodeType::AvatarMixer); - } - } -} - void AvatarData::updateJointMappings() { _jointIndices.clear(); _jointNames.clear(); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 2242860e22..308e0984ef 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -109,7 +109,6 @@ static const float MIN_AVATAR_SCALE = .005f; const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation const int AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS = 1000; -const int AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS = 5000; // See also static AvatarData::defaultFullAvatarModelUrl(). const QString DEFAULT_FULL_AVATAR_MODEL_NAME = QString("Default"); @@ -166,7 +165,6 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName) Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript WRITE setSkeletonModelURLFromScript) Q_PROPERTY(QVector<AttachmentData> attachmentData READ getAttachmentData WRITE setAttachmentData) - Q_PROPERTY(QString billboardURL READ getBillboardURL WRITE setBillboardFromURL) Q_PROPERTY(QStringList jointNames READ getJointNames) @@ -294,8 +292,6 @@ public: bool hasIdentityChangedAfterParsing(const QByteArray& data); QByteArray identityByteArray(); - bool hasBillboardChangedAfterParsing(const QByteArray& data); - const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; } const QString& getDisplayName() const { return _displayName; } virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); @@ -313,12 +309,6 @@ public: Q_INVOKABLE void detachOne(const QString& modelURL, const QString& jointName = QString()); Q_INVOKABLE void detachAll(const QString& modelURL, const QString& jointName = QString()); - virtual void setBillboard(const QByteArray& billboard); - const QByteArray& getBillboard() const { return _billboard; } - - void setBillboardFromURL(const QString& billboardURL); - const QString& getBillboardURL() { return _billboardURL; } - QString getSkeletonModelURLFromScript() const { return _skeletonModelURL.toString(); } void setSkeletonModelURLFromScript(const QString& skeletonModelString) { setSkeletonModelURL(QUrl(skeletonModelString)); } @@ -350,9 +340,7 @@ public: public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); - void sendBillboardPacket(); - void setBillboardFromNetworkReply(); void setJointMappingsFromNetworkReply(); void setSessionUUID(const QUuid& sessionUUID) { setID(sessionUUID); } @@ -391,9 +379,6 @@ protected: float _displayNameTargetAlpha; float _displayNameAlpha; - QByteArray _billboard; - QString _billboardURL; - QHash<QString, int> _jointIndices; ///< 1-based, since zero is returned for missing keys QStringList _jointNames; ///< in order of depth-first traversal diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 612f4c6f96..bd43560ae8 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -140,17 +140,6 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> } } -void AvatarHashMap::processAvatarBillboardPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) { - QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - - auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); - - QByteArray billboard = message->read(message->getBytesLeftToRead()); - if (avatar->getBillboard() != billboard) { - avatar->setBillboard(billboard); - } -} - void AvatarHashMap::processKillAvatar(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) { // read the node id QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index ee1197367c..5f58074427 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -53,7 +53,6 @@ private slots: void processAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode); void processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode); - void processAvatarBillboardPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode); void processKillAvatar(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode); protected: diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 814faa8874..bec2fa9b8d 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -13,6 +13,7 @@ #include <QEventLoop> #include <QScriptSyntaxCheckResult> +#include <QThreadPool> #include <ColorUtils.h> #include <AbstractScriptingServicesInterface.h> @@ -77,9 +78,30 @@ EntityTreeRenderer::~EntityTreeRenderer() { int EntityTreeRenderer::_entitiesScriptEngineCount = 0; -void EntityTreeRenderer::setupEntitiesScriptEngine() { - QSharedPointer<ScriptEngine> oldEngine = _entitiesScriptEngine; // save the old engine through this function, so the EntityScriptingInterface doesn't have problems with it. - _entitiesScriptEngine = QSharedPointer<ScriptEngine>(new ScriptEngine(NO_SCRIPT, QString("Entities %1").arg(++_entitiesScriptEngineCount)), &QObject::deleteLater); +void entitiesScriptEngineDeleter(ScriptEngine* engine) { + class WaitRunnable : public QRunnable { + public: + WaitRunnable(ScriptEngine* engine) : _engine(engine) {} + virtual void run() override { + _engine->waitTillDoneRunning(); + _engine->deleteLater(); + } + + private: + ScriptEngine* _engine; + }; + + // Wait for the scripting thread from the thread pool to avoid hanging the main thread + QThreadPool::globalInstance()->start(new WaitRunnable(engine)); +} + +void EntityTreeRenderer::resetEntitiesScriptEngine() { + // Keep a ref to oldEngine until newEngine is ready so EntityScriptingInterface has something to use + auto oldEngine = _entitiesScriptEngine; + + auto newEngine = new ScriptEngine(NO_SCRIPT, QString("Entities %1").arg(++_entitiesScriptEngineCount)); + _entitiesScriptEngine = QSharedPointer<ScriptEngine>(newEngine, entitiesScriptEngineDeleter); + _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine.data()); _entitiesScriptEngine->runInThread(); DependencyManager::get<EntityScriptingInterface>()->setEntitiesScriptEngine(_entitiesScriptEngine.data()); @@ -87,16 +109,16 @@ void EntityTreeRenderer::setupEntitiesScriptEngine() { void EntityTreeRenderer::clear() { leaveAllEntities(); + if (_entitiesScriptEngine) { + // Unload and stop the engine here (instead of in its deleter) to + // avoid marshalling unload signals back to this thread _entitiesScriptEngine->unloadAllEntityScripts(); _entitiesScriptEngine->stop(); } if (_wantScripts && !_shuttingDown) { - // NOTE: you can't actually need to delete it here because when we call setupEntitiesScriptEngine it will - // assign a new instance to our shared pointer, which will deref the old instance and ultimately call - // the custom deleter which calls deleteLater - setupEntitiesScriptEngine(); + resetEntitiesScriptEngine(); } auto scene = _viewState->getMain3DScene(); @@ -125,7 +147,7 @@ void EntityTreeRenderer::init() { entityTree->setFBXService(this); if (_wantScripts) { - setupEntitiesScriptEngine(); + resetEntitiesScriptEngine(); } forceRecheckEntities(); // setup our state to force checking our inside/outsideness of entities diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a6fc58e5f1..5c06c5f5cc 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -126,7 +126,7 @@ protected: } private: - void setupEntitiesScriptEngine(); + void resetEntitiesScriptEngine(); void addEntityToScene(EntityItemPointer entity); bool findBestZoneAndMaybeContainingEntities(const glm::vec3& avatarPosition, QVector<EntityItemID>* entitiesContainingAvatar); diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index a48f3f7198..84f801b059 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -45,23 +45,17 @@ UpdateEntityOperator::UpdateEntityOperator(EntityTreePointer tree, _newEntityCube = newQueryAACube; _newEntityBox = _newEntityCube.clamp((float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); // clamp to domain bounds - // If our new properties don't have bounds details (no change to position, etc) or if this containing element would - // be the best fit for our new properties, then just do the new portion of the store pass, since the change path will - // be the same for both parts of the update + // set oldElementBestFit true if the entity was in the correct element before this operator was run. bool oldElementBestFit = _containingElement->bestFitBounds(_oldEntityBox); // For some reason we've seen a case where the original containing element isn't a best fit for the old properties // in this case we want to move it, even if the properties haven't changed. - if (oldElementBestFit) { - if (_wantDebug) { - qCDebug(entities) << " **** TYPICAL NO MOVE CASE **** oldElementBestFit:" << oldElementBestFit; - } - } else { + if (!oldElementBestFit) { _oldEntityBox = _existingEntity->getElement()->getAACube(); _removeOld = true; // our properties are going to move us, so remember this for later processing if (_wantDebug) { - qCDebug(entities) << " **** UNUSUAL CASE **** no changes, but not best fit... consider it a move.... **"; + qCDebug(entities) << " **** UNUSUAL CASE **** not best fit.... **"; } } diff --git a/libraries/gpu/src/gpu/GLBackendTexture.cpp b/libraries/gpu/src/gpu/GLBackendTexture.cpp index 609451bd13..24b9544168 100755 --- a/libraries/gpu/src/gpu/GLBackendTexture.cpp +++ b/libraries/gpu/src/gpu/GLBackendTexture.cpp @@ -535,13 +535,12 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GL GLint minFilter; GLint magFilter; }; - static const GLFilterMode filterModes[] = { + static const GLFilterMode filterModes[Sampler::NUM_FILTERS] = { { GL_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_POINT, { GL_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR, { GL_LINEAR, GL_NEAREST }, //FILTER_MIN_LINEAR_MAG_POINT, { GL_LINEAR, GL_LINEAR }, //FILTER_MIN_MAG_LINEAR, - { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_MIP_POINT, { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST }, //FILTER_MIN_MAG_MIP_POINT, { GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST }, //FILTER_MIN_MAG_POINT_MIP_LINEAR, { GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR }, //FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT, @@ -557,7 +556,7 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GL glTexParameteri(object->_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); glTexParameteri(object->_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); - static const GLenum comparisonFuncs[] = { + static const GLenum comparisonFuncs[NUM_COMPARISON_FUNCS] = { GL_NEVER, GL_LESS, GL_EQUAL, @@ -574,7 +573,7 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GL glTexParameteri(object->_target, GL_TEXTURE_COMPARE_MODE, GL_NONE); } - static const GLenum wrapModes[] = { + static const GLenum wrapModes[Sampler::NUM_WRAP_MODES] = { GL_REPEAT, // WRAP_REPEAT, GL_MIRRORED_REPEAT, // WRAP_MIRROR, GL_CLAMP_TO_EDGE, // WRAP_CLAMP, diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index c7fa5f5671..a217041f4e 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -1,5 +1,6 @@ #include "DisplayPlugin.h" +#include <NumericalConstants.h> #include <ui/Menu.h> #include "PluginContainer.h" @@ -23,4 +24,22 @@ void DisplayPlugin::deactivate() { Parent::deactivate(); } +int64_t DisplayPlugin::getPaintDelayUsecs() const { + std::lock_guard<std::mutex> lock(_paintDelayMutex); + return _paintDelayTimer.isValid() ? _paintDelayTimer.nsecsElapsed() / NSECS_PER_USEC : 0; +} +void DisplayPlugin::incrementPresentCount() { +#ifdef DEBUG_PAINT_DELAY + // Avoid overhead if we are not debugging + { + std::lock_guard<std::mutex> lock(_paintDelayMutex); + _paintDelayTimer.start(); + } +#endif + + ++_presentedFrameIndex; + + // Alert the app that it needs to paint a new presentation frame + qApp->postEvent(qApp, new QEvent(static_cast<QEvent::Type>(Present)), Qt::HighEventPriority); +} diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 91dcf9398f..41f380aa86 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -15,6 +15,7 @@ #include <QtCore/QSize> #include <QtCore/QPoint> +#include <QtCore/QElapsedTimer> class QImage; #include <GLMHelpers.h> @@ -59,6 +60,10 @@ class DisplayPlugin : public Plugin { Q_OBJECT using Parent = Plugin; public: + enum Event { + Present = QEvent::User + 1 + }; + bool activate() override; void deactivate() override; virtual bool isHmd() const { return false; } @@ -156,6 +161,8 @@ public: // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } uint32_t presentCount() const { return _presentedFrameIndex; } + // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) + int64_t getPaintDelayUsecs() const; virtual void cycleDebugOutput() {} @@ -165,9 +172,11 @@ signals: void recommendedFramebufferSizeChanged(const QSize & size); protected: - void incrementPresentCount() { ++_presentedFrameIndex; } + void incrementPresentCount(); private: std::atomic<uint32_t> _presentedFrameIndex; + mutable std::mutex _paintDelayMutex; + QElapsedTimer _paintDelayTimer; }; diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index fa166300ae..569063955d 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -101,7 +101,7 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { // Unpack the normal from the map frag.normal = normalize(frag.normalVal.xyz * 2.0 - vec3(1.0)); - frag.roughness = 2.0 * frag.normalVal.a; + frag.roughness = frag.normalVal.a; // Diffuse color and unpack the mode and the metallicness frag.diffuse = frag.diffuseVal.xyz; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f7ac7894ff..15e896fac4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -62,8 +62,6 @@ #include "MIDIEvent.h" -std::atomic<bool> ScriptEngine::_stoppingAllScripts { false }; - static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) @@ -138,10 +136,9 @@ static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName return false; } -ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString, bool wantSignals) : +ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString) : _scriptContents(scriptContents), _timerFunctionMap(), - _wantSignals(wantSignals), _fileNameString(fileNameString), _arrayBufferClass(new ArrayBufferClass(this)) { @@ -155,7 +152,7 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam } ScriptEngine::~ScriptEngine() { - qCDebug(scriptengine) << "Script Engine shutting down (destructor) for script:" << getFilename(); + qCDebug(scriptengine) << "Script Engine shutting down:" << getFilename(); auto scriptEngines = DependencyManager::get<ScriptEngines>(); if (scriptEngines) { @@ -163,16 +160,15 @@ ScriptEngine::~ScriptEngine() { } else { qCWarning(scriptengine) << "Script destroyed after ScriptEngines!"; } - - waitTillDoneRunning(); } void ScriptEngine::disconnectNonEssentialSignals() { disconnect(); - QThread* receiver; + QThread* workerThread; // Ensure the thread should be running, and does exist - if (_isRunning && _isThreaded && (receiver = thread())) { - connect(this, &ScriptEngine::doneRunning, receiver, &QThread::quit); + if (_isRunning && _isThreaded && (workerThread = thread())) { + connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); } } @@ -231,15 +227,14 @@ void ScriptEngine::runDebuggable() { return; } stopAllTimers(); // make sure all our timers are stopped if the script is ending - if (_wantSignals) { - emit scriptEnding(); - emit finished(_fileNameString, this); - } + + emit scriptEnding(); + emit finished(_fileNameString, this); _isRunning = false; - if (_wantSignals) { - emit runningStateChanged(); - emit doneRunning(); - } + + emit runningStateChanged(); + emit doneRunning(); + timer->deleteLater(); return; } @@ -249,9 +244,7 @@ void ScriptEngine::runDebuggable() { if (_lastUpdate < now) { float deltaTime = (float)(now - _lastUpdate) / (float)USECS_PER_SECOND; if (!_isFinished) { - if (_wantSignals) { - emit update(deltaTime); - } + emit update(deltaTime); } } _lastUpdate = now; @@ -272,57 +265,72 @@ void ScriptEngine::runInThread() { } _isThreaded = true; - QThread* workerThread = new QThread(); - QString scriptEngineName = QString("Script Thread:") + getFilename(); - workerThread->setObjectName(scriptEngineName); + // The thread interface cannot live on itself, and we want to move this into the thread, so + // the thread cannot have this as a parent. + QThread* workerThread = new QThread(); + workerThread->setObjectName(QString("Script Thread:") + getFilename()); + moveToThread(workerThread); + // NOTE: If you connect any essential signals for proper shutdown or cleanup of // the script engine, make sure to add code to "reconnect" them to the // disconnectNonEssentialSignals() method - - // when the worker thread is started, call our engine's run.. connect(workerThread, &QThread::started, this, &ScriptEngine::run); - - // tell the thread to stop when the script engine is done connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater); - moveToThread(workerThread); - - // Starts an event loop, and emits workerThread->started() workerThread->start(); } void ScriptEngine::waitTillDoneRunning() { - // If the script never started running or finished running before we got here, we don't need to wait for it auto workerThread = thread(); - if (_isThreaded && workerThread) { - QString scriptName = getFilename(); - auto startedWaiting = usecTimestampNow(); + if (_isThreaded && workerThread) { + // We should never be waiting (blocking) on our own thread + assert(workerThread != QThread::currentThread()); + + // Engine should be stopped already, but be defensive + stop(); + + auto startedWaiting = usecTimestampNow(); while (workerThread->isRunning()) { // NOTE: This will be called on the main application thread from stopAllScripts. // The application thread will need to continue to process events, because // the scripts will likely need to marshall messages across to the main thread, e.g. // if they access Settings or Menu in any of their shutdown code. So: // Process events for the main application thread, allowing invokeMethod calls to pass between threads. - QCoreApplication::processEvents(); // thread-safe :) + QCoreApplication::processEvents(); - // If we've been waiting a second or more, then tell the script engine to stop evaluating - static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; + // If the final evaluation takes too long, then tell the script engine to stop running auto elapsedUsecs = usecTimestampNow() - startedWaiting; + static const auto MAX_SCRIPT_EVALUATION_TIME = USECS_PER_SECOND; if (elapsedUsecs > MAX_SCRIPT_EVALUATION_TIME) { - qCDebug(scriptengine) << - "Script " << scriptName << " has been running too long [" << elapsedUsecs << " usecs] quitting."; - abortEvaluation(); // to allow the thread to quit workerThread->quit(); - break; + + if (isEvaluating()) { + qCWarning(scriptengine) << "Script Engine has been running too long, aborting:" << getFilename(); + abortEvaluation(); + } else { + qCWarning(scriptengine) << "Script Engine has been running too long, throwing:" << getFilename(); + auto context = currentContext(); + if (context) { + context->throwError("Timed out during shutdown"); + } + } + + // Wait for the scripting thread to stop running, as + // flooding it with aborts/exceptions will persist it longer + static const auto MAX_SCRIPT_QUITTING_TIME = 0.5 * MSECS_PER_SECOND; + if (workerThread->wait(MAX_SCRIPT_QUITTING_TIME)) { + workerThread->terminate(); + } } // Avoid a pure busy wait QThread::yieldCurrentThread(); } - workerThread->deleteLater(); + qCDebug(scriptengine) << "Script Engine has stopped:" << getFilename(); } } @@ -358,17 +366,13 @@ void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scrip if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) { _debuggable = true; } - if (_wantSignals) { - emit scriptLoaded(url.toString()); - } + emit scriptLoaded(url.toString()); } // FIXME - switch this to the new model of ScriptCache callbacks void ScriptEngine::errorInLoadingScript(const QUrl& url) { qCDebug(scriptengine) << "ERROR Loading file:" << url.toString() << "line:" << __LINE__; - if (_wantSignals) { - emit errorLoadingScript(_fileNameString); // ?? - } + emit errorLoadingScript(_fileNameString); // ?? } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of @@ -756,7 +760,7 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { - if (_stoppingAllScripts) { + if (DependencyManager::get<ScriptEngines>()->isStopped()) { return QScriptValue(); // bail early } @@ -785,14 +789,12 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi --_evaluatesPending; const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName()); - if (_wantSignals) { - emit evaluationFinished(result, hadUncaughtException); - } + emit evaluationFinished(result, hadUncaughtException); return result; } void ScriptEngine::run() { - if (_stoppingAllScripts) { + if (DependencyManager::get<ScriptEngines>()->isStopped()) { return; // bail early - avoid setting state in init(), as evaluate() will bail too } @@ -801,9 +803,7 @@ void ScriptEngine::run() { } _isRunning = true; - if (_wantSignals) { - emit runningStateChanged(); - } + emit runningStateChanged(); QScriptValue result = evaluate(_scriptContents, _fileNameString); @@ -872,9 +872,7 @@ void ScriptEngine::run() { if (_lastUpdate < now) { float deltaTime = (float) (now - _lastUpdate) / (float) USECS_PER_SECOND; if (!_isFinished) { - if (_wantSignals) { - emit update(deltaTime); - } + emit update(deltaTime); } } _lastUpdate = now; @@ -883,10 +881,10 @@ void ScriptEngine::run() { hadUncaughtExceptions(*this, _fileNameString); } + qCDebug(scriptengine) << "Script Engine stopping:" << getFilename(); + stopAllTimers(); // make sure all our timers are stopped if the script is ending - if (_wantSignals) { - emit scriptEnding(); - } + emit scriptEnding(); if (entityScriptingInterface->getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. @@ -904,15 +902,11 @@ void ScriptEngine::run() { } } - if (_wantSignals) { - emit finished(_fileNameString, this); - } + emit finished(_fileNameString, this); _isRunning = false; - if (_wantSignals) { - emit runningStateChanged(); - emit doneRunning(); - } + emit runningStateChanged(); + emit doneRunning(); } // NOTE: This is private because it must be called on the same thread that created the timers, which is why @@ -945,14 +939,8 @@ void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { void ScriptEngine::stop() { if (!_isFinished) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "stop"); - return; - } _isFinished = true; - if (_wantSignals) { - emit runningStateChanged(); - } + emit runningStateChanged(); } } @@ -1025,7 +1013,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int } QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { - if (_stoppingAllScripts) { + if (DependencyManager::get<ScriptEngines>()->isStopped()) { qCDebug(scriptengine) << "Script.setInterval() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } @@ -1034,7 +1022,7 @@ QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) } QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { - if (_stoppingAllScripts) { + if (DependencyManager::get<ScriptEngines>()->isStopped()) { qCDebug(scriptengine) << "Script.setTimeout() while shutting down is ignored... parent script:" << getFilename(); return NULL; // bail early } @@ -1076,9 +1064,7 @@ QUrl ScriptEngine::resolvePath(const QString& include) const { } void ScriptEngine::print(const QString& message) { - if (_wantSignals) { - emit printedMessage(message); - } + emit printedMessage(message); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1086,7 +1072,7 @@ void ScriptEngine::print(const QString& message) { // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { - if (_stoppingAllScripts) { + if (DependencyManager::get<ScriptEngines>()->isStopped()) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored..." << "includeFiles:" << includeFiles << "parent script:" << getFilename(); return; // bail early @@ -1184,7 +1170,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac } void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { - if (_stoppingAllScripts) { + if (DependencyManager::get<ScriptEngines>()->isStopped()) { qCDebug(scriptengine) << "Script.include() while shutting down is ignored... " << "includeFile:" << includeFile << "parent script:" << getFilename(); return; // bail early @@ -1199,7 +1185,7 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { - if (_stoppingAllScripts) { + if (DependencyManager::get<ScriptEngines>()->isStopped()) { qCDebug(scriptengine) << "Script.load() while shutting down is ignored... " << "loadFile:" << loadFile << "parent script:" << getFilename(); return; // bail early @@ -1214,13 +1200,9 @@ void ScriptEngine::load(const QString& loadFile) { if (_isReloading) { auto scriptCache = DependencyManager::get<ScriptCache>(); scriptCache->deleteScript(url.toString()); - if (_wantSignals) { - emit reloadScript(url.toString(), false); - } + emit reloadScript(url.toString(), false); } else { - if (_wantSignals) { - emit loadScript(url.toString(), false); - } + emit loadScript(url.toString(), false); } } @@ -1309,8 +1291,23 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co setParentURL(scriptOrURL); } + const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; QScriptEngine sandbox; - QScriptValue testConstructor = sandbox.evaluate(program); + sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); + QScriptValue testConstructor; + { + QTimer timeout; + timeout.setSingleShot(true); + timeout.start(SANDBOX_TIMEOUT); + connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{ + auto context = sandbox.currentContext(); + if (context) { + // Guard against infinite loops and non-performant code + context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); + } + }); + testConstructor = sandbox.evaluate(program); + } if (hadUncaughtExceptions(sandbox, program.fileName())) { return; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index d37e3eb177..80978e4527 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -67,10 +67,7 @@ public: class ScriptEngine : public QScriptEngine, public ScriptUser, public EntitiesScriptEngineProvider { Q_OBJECT public: - ScriptEngine(const QString& scriptContents = NO_SCRIPT, - const QString& fileNameString = QString(""), - bool wantSignals = true); - + ScriptEngine(const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("")); ~ScriptEngine(); /// run the script in a dedicated thread. This will have the side effect of evalulating @@ -83,6 +80,15 @@ public: /// run the script in the callers thread, exit when stop() is called. void run(); + QString getFilename() const; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts + Q_INVOKABLE void stop(); + + // Stop any evaluating scripts and wait for the scripting thread to finish. + void waitTillDoneRunning(); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - these are NOT intended to be public interfaces available to scripts, the are only Q_INVOKABLE so we can // properly ensure they are only called on the correct thread @@ -138,10 +144,6 @@ public: Q_INVOKABLE void requestGarbageCollection() { collectGarbage(); } - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts - Q_INVOKABLE void stop(); - bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget @@ -191,7 +193,6 @@ protected: bool _isInitialized { false }; QHash<QTimer*, CallbackData> _timerFunctionMap; QSet<QUrl> _includedURLs; - bool _wantSignals { true }; QHash<EntityItemID, EntityScriptDetails> _entityScripts; bool _isThreaded { false }; QScriptEngineDebugger* _debugger { nullptr }; @@ -199,8 +200,7 @@ protected: qint64 _lastUpdate; void init(); - QString getFilename() const; - void waitTillDoneRunning(); + bool evaluatePending() const { return _evaluatesPending > 0; } void timerFired(); void stopAllTimers(); @@ -232,9 +232,6 @@ protected: QUrl currentSandboxURL {}; // The toplevel url string for the entity script that loaded the code being executed, else empty. void doWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, std::function<void()> operation); void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args); - - friend class ScriptEngines; - static std::atomic<bool> _stoppingAllScripts; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 330a94cf0b..70eb055d22 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -119,26 +119,27 @@ void ScriptEngines::registerScriptInitializer(ScriptInitializer initializer) { } void ScriptEngines::addScriptEngine(ScriptEngine* engine) { - _allScriptsMutex.lock(); - _allKnownScriptEngines.insert(engine); - _allScriptsMutex.unlock(); + if (_isStopped) { + engine->deleteLater(); + } else { + QMutexLocker locker(&_allScriptsMutex); + _allKnownScriptEngines.insert(engine); + } } void ScriptEngines::removeScriptEngine(ScriptEngine* engine) { // If we're not already in the middle of stopping all scripts, then we should remove ourselves // from the list of running scripts. We don't do this if we're in the process of stopping all scripts // because that method removes scripts from its list as it iterates them - if (!_stoppingAllScripts) { - _allScriptsMutex.lock(); + if (!_isStopped) { + QMutexLocker locker(&_allScriptsMutex); _allKnownScriptEngines.remove(engine); - _allScriptsMutex.unlock(); } } void ScriptEngines::shutdownScripting() { - _allScriptsMutex.lock(); - _stoppingAllScripts = true; - ScriptEngine::_stoppingAllScripts = true; + _isStopped = true; + QMutexLocker locker(&_allScriptsMutex); qCDebug(scriptengine) << "Stopping all scripts.... currently known scripts:" << _allKnownScriptEngines.size(); QMutableSetIterator<ScriptEngine*> i(_allKnownScriptEngines); @@ -149,6 +150,7 @@ void ScriptEngines::shutdownScripting() { // NOTE: typically all script engines are running. But there's at least one known exception to this, the // "entities sandbox" which is only used to evaluate entities scripts to test their validity before using // them. We don't need to stop scripts that aren't running. + // TODO: Scripts could be shut down faster if we spread them across a threadpool. if (scriptEngine->isRunning()) { qCDebug(scriptengine) << "about to shutdown script:" << scriptName; @@ -157,8 +159,7 @@ void ScriptEngines::shutdownScripting() { // and stop. We can safely short circuit this because we know we're in the "quitting" process scriptEngine->disconnect(this); - // Calling stop on the script engine will set it's internal _isFinished state to true, and result - // in the ScriptEngine gracefully ending it's run() method. + // Gracefully stop the engine's scripting thread scriptEngine->stop(); // We need to wait for the engine to be done running before we proceed, because we don't @@ -170,12 +171,10 @@ void ScriptEngines::shutdownScripting() { scriptEngine->deleteLater(); - // If the script is stopped, we can remove it from our set + // Once the script is stopped, we can remove it from our set i.remove(); } } - _stoppingAllScripts = false; - _allScriptsMutex.unlock(); qCDebug(scriptengine) << "DONE Stopping all scripts...."; } @@ -428,7 +427,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL return scriptEngine; } - scriptEngine = new ScriptEngine(NO_SCRIPT, "", true); + scriptEngine = new ScriptEngine(NO_SCRIPT, ""); scriptEngine->setUserLoaded(isUserLoaded); connect(scriptEngine, &ScriptEngine::doneRunning, this, [scriptEngine] { scriptEngine->deleteLater(); @@ -499,7 +498,6 @@ void ScriptEngines::launchScriptEngine(ScriptEngine* scriptEngine) { } } - void ScriptEngines::onScriptFinished(const QString& rawScriptURL, ScriptEngine* engine) { bool removed = false; { diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 6522aa9bb3..72bf7d529e 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -86,16 +86,17 @@ protected: void onScriptEngineLoaded(const QString& scriptFilename); void onScriptEngineError(const QString& scriptFilename); void launchScriptEngine(ScriptEngine* engine); + bool isStopped() const { return _isStopped; } QReadWriteLock _scriptEnginesHashLock; QHash<QUrl, ScriptEngine*> _scriptEnginesHash; QSet<ScriptEngine*> _allKnownScriptEngines; QMutex _allScriptsMutex; - std::atomic<bool> _stoppingAllScripts { false }; std::list<ScriptInitializer> _scriptInitializers; mutable Setting::Handle<QString> _scriptsLocationHandle; ScriptsModel _scriptsModel; ScriptsModelFilter _scriptsModelFilter; + std::atomic<bool> _isStopped { false }; }; QUrl normalizeScriptURL(const QUrl& rawScriptURL); diff --git a/scripts/system/users.js b/scripts/system/users.js index 9612a19eee..d935dd23ca 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var PopUpMenu = function(properties) { +var PopUpMenu = function (properties) { var value = properties.value, promptOverlay, valueOverlay, @@ -21,9 +21,8 @@ var PopUpMenu = function(properties) { MIN_MAX_BUTTON_SVG_WIDTH = 17.1, MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, MIN_MAX_BUTTON_WIDTH = 14, - MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH; - - MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); + MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH, + MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); function positionDisplayOptions() { var y, @@ -203,7 +202,7 @@ var PopUpMenu = function(properties) { width: MIN_MAX_BUTTON_SVG_WIDTH, height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 }, - color: properties.buttonColor, + //color: properties.buttonColor, alpha: properties.buttonAlpha, visible: properties.visible }); @@ -218,11 +217,10 @@ var PopUpMenu = function(properties) { }; }; -var usersWindow = (function() { +var usersWindow = (function () { - var baseURL = Script.resolvePath("assets/images/tools/"); - - var WINDOW_WIDTH = 260, + var baseURL = Script.resolvePath("assets/images/tools/"), + WINDOW_WIDTH = 260, WINDOW_MARGIN = 12, WINDOW_BASE_MARGIN = 6, // A little less is needed in order look correct WINDOW_FONT = { @@ -248,6 +246,17 @@ var usersWindow = (function() { WINDOW_BACKGROUND_ALPHA = 0.8, windowPane, windowHeading, + + // Window border is similar to that of edit.js. + WINDOW_BORDER_WIDTH = WINDOW_WIDTH + 2 * WINDOW_BASE_MARGIN, + WINDOW_BORDER_TOP_MARGIN = 2 * WINDOW_BASE_MARGIN, + WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_BASE_MARGIN, + WINDOW_BORDER_LEFT_MARGIN = WINDOW_BASE_MARGIN, + WINDOW_BORDER_RADIUS = 4, + WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, + WINDOW_BORDER_ALPHA = 0.5, + windowBorder, + MIN_MAX_BUTTON_SVG = baseURL + "min-max-toggle.svg", MIN_MAX_BUTTON_SVG_WIDTH = 17.1, MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, @@ -331,6 +340,7 @@ var usersWindow = (function() { visibilityControl, windowHeight, + windowBorderHeight, windowTextHeight, windowLineSpacing, windowLineHeight, // = windowTextHeight + windowLineSpacing @@ -356,14 +366,21 @@ var usersWindow = (function() { MENU_ITEM_AFTER = "Chat...", SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", + SETINGS_USERS_WINDOW_OFFSET = "UsersWindow.Offset", + // +ve x, y values are offset from left, top of screen; -ve from right, bottom. isVisible = true, isMinimized = false, + isBorderVisible = false, - viewportHeight, + viewport, isMirrorDisplay = false, isFullscreenMirror = false, + windowPosition = { }, // Bottom left corner of window pane. + isMovingWindow = false, + movingClickOffset = { x: 0, y: 0 }, + isUsingScrollbars = false, isMovingScrollbar = false, scrollbarBackgroundPosition = {}, @@ -379,19 +396,23 @@ var usersWindow = (function() { if (isMinimized) { windowHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; + windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; return; } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + windowLineHeight + VISIBILITY_SPACER + windowLineHeight + WINDOW_BASE_MARGIN; + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + + windowLineHeight + VISIBILITY_SPACER + + windowLineHeight + WINDOW_BASE_MARGIN; - // Limit window to height of viewport minus VU meter and mirror if displayed + // Limit window to height of viewport above window position minus VU meter and mirror if displayed windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; - maxWindowHeight = viewportHeight - AUDIO_METER_HEIGHT; + maxWindowHeight = windowPosition.y - AUDIO_METER_HEIGHT; if (isMirrorDisplay && !isFullscreenMirror) { maxWindowHeight -= MIRROR_HEIGHT; } windowHeight = Math.max(Math.min(windowHeight, maxWindowHeight), nonUsersHeight); + windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; // Corresponding number of users to actually display numUsersToDisplay = Math.max(Math.round((windowHeight - nonUsersHeight) / windowLineHeight), 0); @@ -405,38 +426,57 @@ var usersWindow = (function() { } function updateOverlayPositions() { - var y; + // Overlay positions are all relative to windowPosition; windowPosition is the position of the windowPane overlay. + var windowLeft = windowPosition.x, + windowTop = windowPosition.y - windowHeight, + x, + y; + Overlays.editOverlay(windowBorder, { + x: windowPosition.x - WINDOW_BORDER_LEFT_MARGIN, + y: windowTop - WINDOW_BORDER_TOP_MARGIN + }); Overlays.editOverlay(windowPane, { - y: viewportHeight - windowHeight + x: windowLeft, + y: windowTop }); Overlays.editOverlay(windowHeading, { - y: viewportHeight - windowHeight + WINDOW_MARGIN + x: windowLeft + WINDOW_MARGIN, + y: windowTop + WINDOW_MARGIN }); Overlays.editOverlay(minimizeButton, { - y: viewportHeight - windowHeight + WINDOW_MARGIN / 2 + x: windowLeft + WINDOW_WIDTH - WINDOW_MARGIN / 2 - MIN_MAX_BUTTON_WIDTH, + y: windowTop + WINDOW_MARGIN / 2 }); - scrollbarBackgroundPosition.y = viewportHeight - windowHeight + WINDOW_MARGIN + windowTextHeight; + scrollbarBackgroundPosition.x = windowLeft + WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH; + scrollbarBackgroundPosition.y = windowTop + WINDOW_MARGIN + windowTextHeight; Overlays.editOverlay(scrollbarBackground, { + x: scrollbarBackgroundPosition.x, y: scrollbarBackgroundPosition.y }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); Overlays.editOverlay(scrollbarBar, { + x: scrollbarBackgroundPosition.x + 1, y: scrollbarBarPosition.y }); - y = viewportHeight - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER - windowLineHeight - VISIBILITY_SPACER - windowLineHeight - WINDOW_BASE_MARGIN; + x = windowLeft + WINDOW_MARGIN; + y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER + - windowLineHeight - VISIBILITY_SPACER + - windowLineHeight - WINDOW_BASE_MARGIN; Overlays.editOverlay(friendsButton, { + x: x, y: y }); y += FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER; - displayControl.updatePosition(WINDOW_MARGIN, y); + displayControl.updatePosition(x, y); y += windowLineHeight + VISIBILITY_SPACER; - visibilityControl.updatePosition(WINDOW_MARGIN, y); + visibilityControl.updatePosition(x, y); } function updateUsersDisplay() { @@ -487,6 +527,10 @@ var usersWindow = (function() { }); } + Overlays.editOverlay(windowBorder, { + height: windowBorderHeight + }); + Overlays.editOverlay(windowPane, { height: windowHeight, text: displayText @@ -512,7 +556,7 @@ var usersWindow = (function() { usersRequest.send(); } - processUsers = function() { + processUsers = function () { var response, myUsername, user, @@ -565,12 +609,15 @@ var usersWindow = (function() { } }; - pollUsersTimedOut = function() { + pollUsersTimedOut = function () { print("Error: Request for users status timed out"); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; function updateOverlayVisibility() { + Overlays.editOverlay(windowBorder, { + visible: isVisible && isBorderVisible + }); Overlays.editOverlay(windowPane, { visible: isVisible }); @@ -670,7 +717,7 @@ var usersWindow = (function() { if (clickedOverlay === windowPane) { overlayX = event.x - WINDOW_MARGIN; - overlayY = event.y - viewportHeight + windowHeight - WINDOW_MARGIN - windowLineHeight; + overlayY = event.y - windowPosition.y + windowHeight - WINDOW_MARGIN - windowLineHeight; numLinesBefore = Math.round(overlayY / windowLineHeight); minY = numLinesBefore * windowLineHeight; @@ -683,7 +730,8 @@ var usersWindow = (function() { userClicked = firstUserToDisplay + lineClicked; - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX + && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } @@ -735,13 +783,29 @@ var usersWindow = (function() { friendsWindow.setURL(FRIENDS_WINDOW_URL); friendsWindow.setVisible(true); friendsWindow.raise(); + return; + } + + if (clickedOverlay === windowBorder) { + movingClickOffset = { + x: event.x - windowPosition.x, + y: event.y - windowPosition.y + }; + + isMovingWindow = true; } } function onMouseMoveEvent(event) { + var isVisible; + if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x + && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN + && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y + && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) + / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); updateOverlayPositions(); @@ -753,35 +817,95 @@ var usersWindow = (function() { isMovingScrollbar = false; } } + + if (isMovingWindow) { + windowPosition = { + x: event.x - movingClickOffset.x, + y: event.y - movingClickOffset.y + }; + calculateWindowHeight(); + updateOverlayPositions(); + updateUsersDisplay(); + + } else { + + isVisible = isBorderVisible; + if (isVisible) { + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x + && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH + && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y + && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + } else { + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH + && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + } + if (isVisible !== isBorderVisible) { + isBorderVisible = isVisible; + Overlays.editOverlay(windowBorder, { + visible: isBorderVisible + }); + } + } } function onMouseReleaseEvent() { - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_ALPHA - }); - isMovingScrollbar = false; + var offset = {}; + + if (isMovingScrollbar) { + Overlays.editOverlay(scrollbarBar, { + backgroundAlpha: SCROLLBAR_BAR_ALPHA + }); + isMovingScrollbar = false; + } + + if (isMovingWindow) { + // Save offset of bottom of window to nearest edge of the window. + offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? windowPosition.x : windowPosition.x - viewport.x; + offset.y = (windowPosition.y < viewport.y / 2) ? windowPosition.y : windowPosition.y - viewport.y; + Settings.setValue(SETINGS_USERS_WINDOW_OFFSET, JSON.stringify(offset)); + isMovingWindow = false; + } } function onScriptUpdate() { - var oldViewportHeight = viewportHeight, + var oldViewport = viewport, oldIsMirrorDisplay = isMirrorDisplay, oldIsFullscreenMirror = isFullscreenMirror, MIRROR_MENU_ITEM = "Mirror", FULLSCREEN_MIRROR_MENU_ITEM = "Fullscreen Mirror"; - viewportHeight = Controller.getViewportDimensions().y; + viewport = Controller.getViewportDimensions(); isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - if (viewportHeight !== oldViewportHeight || isMirrorDisplay !== oldIsMirrorDisplay || isFullscreenMirror !== oldIsFullscreenMirror) { + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay + || isFullscreenMirror !== oldIsFullscreenMirror) { calculateWindowHeight(); updateUsersDisplay(); - updateOverlayPositions(); } + + if (viewport.y !== oldViewport.y) { + if (windowPosition.y > oldViewport.y / 2) { + // Maintain position w.r.t. bottom of window. + windowPosition.y = viewport.y - (oldViewport.y - windowPosition.y); + } + } + + if (viewport.x !== oldViewport.x) { + if (windowPosition.x + (WINDOW_WIDTH / 2) > oldViewport.x / 2) { + // Maintain position w.r.t. right of window. + windowPosition.x = viewport.x - (oldViewport.x - windowPosition.x); + } + } + + updateOverlayPositions(); } function setUp() { - var textSizeOverlay; + var textSizeOverlay, + offsetSetting, + offset = {}, + hmdViewport; textSizeOverlay = Overlays.addOverlay("text", { font: WINDOW_FONT, @@ -792,13 +916,40 @@ var usersWindow = (function() { windowLineHeight = windowTextHeight + windowLineSpacing; Overlays.deleteOverlay(textSizeOverlay); - viewportHeight = Controller.getViewportDimensions().y; + viewport = Controller.getViewportDimensions(); + + offsetSetting = Settings.getValue(SETINGS_USERS_WINDOW_OFFSET); + if (offsetSetting !== "") { + offset = JSON.parse(Settings.getValue(SETINGS_USERS_WINDOW_OFFSET)); + } + if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { + windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; + windowPosition.y = offset.y <= 0 ? viewport.y + offset.y : offset.y; + + } else { + hmdViewport = Controller.getRecommendedOverlayRect(); + windowPosition = { + x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. + y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. + }; + } calculateWindowHeight(); + windowBorder = Overlays.addOverlay("rectangle", { + x: 0, + y: viewport.y, // Start up off-screen + width: WINDOW_BORDER_WIDTH, + height: windowBorderHeight, + radius: WINDOW_BORDER_RADIUS, + color: WINDOW_BORDER_COLOR, + alpha: WINDOW_BORDER_ALPHA, + visible: isVisible && isBorderVisible + }); + windowPane = Overlays.addOverlay("text", { x: 0, - y: viewportHeight, // Start up off-screen + y: viewport.y, width: WINDOW_WIDTH, height: windowHeight, topMargin: WINDOW_MARGIN + windowLineHeight, @@ -813,8 +964,8 @@ var usersWindow = (function() { }); windowHeading = Overlays.addOverlay("text", { - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: WINDOW_WIDTH - 2 * WINDOW_MARGIN, height: windowTextHeight, topMargin: 0, @@ -828,8 +979,8 @@ var usersWindow = (function() { }); minimizeButton = Overlays.addOverlay("image", { - x: WINDOW_WIDTH - WINDOW_MARGIN / 2 - MIN_MAX_BUTTON_WIDTH, - y: viewportHeight, + x: 0, + y: viewport.y, width: MIN_MAX_BUTTON_WIDTH, height: MIN_MAX_BUTTON_HEIGHT, imageURL: MIN_MAX_BUTTON_SVG, @@ -845,11 +996,11 @@ var usersWindow = (function() { }); scrollbarBackgroundPosition = { - x: WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH, - y: viewportHeight + x: 0, + y: viewport.y }; scrollbarBackground = Overlays.addOverlay("text", { - x: scrollbarBackgroundPosition.x, + x: 0, y: scrollbarBackgroundPosition.y, width: SCROLLBAR_BACKGROUND_WIDTH, height: windowTextHeight, @@ -860,11 +1011,11 @@ var usersWindow = (function() { }); scrollbarBarPosition = { - x: WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH + 1, - y: viewportHeight + x: 0, + y: viewport.y }; scrollbarBar = Overlays.addOverlay("text", { - x: scrollbarBarPosition.x, + x: 0, y: scrollbarBarPosition.y, width: SCROLLBAR_BACKGROUND_WIDTH - 2, height: windowTextHeight, @@ -875,8 +1026,8 @@ var usersWindow = (function() { }); friendsButton = Overlays.addOverlay("image", { - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: FRIENDS_BUTTON_WIDTH, height: FRIENDS_BUTTON_HEIGHT, imageURL: FRIENDS_BUTTON_SVG, @@ -895,8 +1046,8 @@ var usersWindow = (function() { value: DISPLAY_VALUES[0], values: DISPLAY_VALUES, displayValues: DISPLAY_DISPLAY_VALUES, - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, promptWidth: DISPLAY_PROMPT_WIDTH, lineHeight: windowLineHeight, @@ -928,8 +1079,8 @@ var usersWindow = (function() { value: myVisibility, values: VISIBILITY_VALUES, displayValues: VISIBILITY_DISPLAY_VALUES, - x: WINDOW_MARGIN, - y: viewportHeight, + x: 0, + y: viewport.y, width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, promptWidth: VISIBILITY_PROMPT_WIDTH, lineHeight: windowLineHeight, @@ -979,6 +1130,7 @@ var usersWindow = (function() { Menu.removeMenuItem(MENU_NAME, MENU_ITEM); Script.clearTimeout(usersTimer); + Overlays.deleteOverlay(windowBorder); Overlays.deleteOverlay(windowPane); Overlays.deleteOverlay(windowHeading); Overlays.deleteOverlay(minimizeButton); @@ -991,4 +1143,4 @@ var usersWindow = (function() { setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}());