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
+}());