diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp
index 93b9b10eb7..9ed6c7fdbc 100644
--- a/assignment-client/src/audio/AudioMixer.cpp
+++ b/assignment-client/src/audio/AudioMixer.cpp
@@ -127,7 +127,7 @@ void AudioMixer::queueReplicatedAudioPacket(QSharedPointer<ReceivedMessage> mess
     // construct a "fake" audio received message from the byte array and packet list information
     auto audioData = message->getMessage().mid(NUM_BYTES_RFC4122_UUID);
 
-    PacketType rewrittenType = REPLICATED_PACKET_MAPPING.key(message->getType());
+    PacketType rewrittenType = PacketTypeEnum::getReplicatedPacketMapping().key(message->getType());
 
     if (rewrittenType == PacketType::Unknown) {
         qDebug() << "Cannot unwrap replicated packet type not present in REPLICATED_PACKET_WRAPPING";
diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp
index 408ddf038c..9bba9c7f30 100644
--- a/assignment-client/src/audio/AudioMixerClientData.cpp
+++ b/assignment-client/src/audio/AudioMixerClientData.cpp
@@ -125,11 +125,11 @@ void AudioMixerClientData::optionallyReplicatePacket(ReceivedMessage& message, c
         // now make sure it's a packet type that we want to replicate
 
         // first check if it is an original type that we should replicate
-        PacketType mirroredType = REPLICATED_PACKET_MAPPING.value(message.getType());
+        PacketType mirroredType = PacketTypeEnum::getReplicatedPacketMapping().value(message.getType());
 
         if (mirroredType == PacketType::Unknown) {
             // if it wasn't check if it is a replicated type that we should re-replicate
-            if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
+            if (PacketTypeEnum::getReplicatedPacketMapping().key(message.getType()) != PacketType::Unknown) {
                 mirroredType = message.getType();
             } else {
                 qDebug() << "Packet passed to optionallyReplicatePacket was not a replicatable type - returning";
diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index eea44f031e..c67e998dd4 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -144,10 +144,10 @@ void AvatarMixer::optionallyReplicatePacket(ReceivedMessage& message, const Node
         // check if this is a packet type we replicate
         // which means it must be a packet type present in REPLICATED_PACKET_MAPPING or must be the
         // replicated version of one of those packet types
-        PacketType replicatedType = REPLICATED_PACKET_MAPPING.value(message.getType());
+        PacketType replicatedType = PacketTypeEnum::getReplicatedPacketMapping().value(message.getType());
 
         if (replicatedType == PacketType::Unknown) {
-            if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
+            if (PacketTypeEnum::getReplicatedPacketMapping().key(message.getType()) != PacketType::Unknown) {
                 replicatedType = message.getType();
             } else {
                 qDebug() << __FUNCTION__ << "called without replicatable packet type - returning";
diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json
index a493d8e9af..ee2b916d1e 100644
--- a/interface/resources/avatar/avatar-animation.json
+++ b/interface/resources/avatar/avatar-animation.json
@@ -126,24 +126,6 @@
                                     "weightVar": "headWeight",
                                     "weight": 4.0,
                                     "flexCoefficients": [1, 0.5, 0.25, 0.2, 0.1]
-                                },
-                                {
-                                    "jointName": "LeftArm",
-                                    "positionVar": "leftArmPosition",
-                                    "rotationVar": "leftArmRotation",
-                                    "typeVar": "leftArmType",
-                                    "weightVar": "leftArmWeight",
-                                    "weight": 0.75,
-                                    "flexCoefficients": [1.0, 0.35, 0.2, 0.1, 0.05, 0.0, 0.0, 0.0]
-                                },
-                                {
-                                    "jointName": "RightArm",
-                                    "positionVar": "rightArmPosition",
-                                    "rotationVar": "rightArmRotation",
-                                    "typeVar": "rightArmType",
-                                    "weightVar": "rightArmWeight",
-                                    "weight": 0.75,
-                                    "flexCoefficients": [1.0, 0.35, 0.2, 0.1, 0.05, 0.0, 0.0, 0.0]
                                 }
                             ]
                         },
diff --git a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml
index 4adb485c2b..62cd581e3b 100644
--- a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml
+++ b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml
@@ -50,7 +50,7 @@ Item {
                         margins: 4
                     }
                     clip: true
-                    snapMode: ListView.SnapToItem
+                    cacheBuffer: 4000
 
                     model: ListModel {}
                     delegate: Item {
diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml
index 90d6ba7022..78c10e2ffa 100644
--- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml
+++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml
@@ -145,12 +145,13 @@ Rectangle {
         visible: headPuckBox.checked
         HifiControls.SpinBox {
             id: headYOffset
-            decimals: 4
+            decimals: 1
             width: 112
-            label: "Y: offset"
+            label: "Y Offset"
+            suffix: " cm"
             minimumValue: -10
-            stepSize: 0.0254
-            value: -0.05
+            stepSize: 1
+            value: -5
             colorScheme: hifi.colorSchemes.dark
 
             onEditingFinished: {
@@ -162,11 +163,12 @@ Rectangle {
         HifiControls.SpinBox {
             id: headZOffset
             width: 112
-            label: "Z: offset"
+            label: "Z Offset"
             minimumValue: -10
-            stepSize: 0.0254
-            decimals: 4
-            value: -0.05
+            stepSize: 1
+            decimals: 1
+            suffix: " cm"
+            value: -5
             colorScheme: hifi.colorSchemes.dark
 
             onEditingFinished: {
@@ -175,7 +177,6 @@ Rectangle {
         }
     }
 
-
     RalewayBold {
         id: hands
 
@@ -254,11 +255,12 @@ Rectangle {
 
         HifiControls.SpinBox {
             id: handYOffset
-            decimals: 4
+            decimals: 1
             width: 112
-            label: "Y: offset"
+            suffix: " cm"
+            label: "Y Offset"
             minimumValue: -10
-            stepSize: 0.0254
+            stepSize: 1
             colorScheme: hifi.colorSchemes.dark
 
             onEditingFinished: {
@@ -270,10 +272,11 @@ Rectangle {
         HifiControls.SpinBox {
             id: handZOffset
             width: 112
-            label: "Z: offset"
+            label: "Z Offset"
+            suffix: " cm"
             minimumValue: -10
-            stepSize: 0.0254
-            decimals: 4
+            stepSize: 1
+            decimals: 1
             colorScheme: hifi.colorSchemes.dark
 
             onEditingFinished: {
@@ -488,15 +491,55 @@ Rectangle {
         }
     }
 
+    Row {
+        id: shoulderAdditionalConfig
+        visible: shoulderBox.checked
+        anchors.top: shoulderConfig.bottom
+        anchors.topMargin: 5
+        anchors.left: openVrConfiguration.left
+        anchors.leftMargin: leftMargin + 20
+        spacing: 10
+
+        HifiControls.SpinBox {
+            id: armCircumference
+            decimals: 1
+            width: 160
+            suffix: " cm"
+            label: "Arm Circumference"
+            minimumValue: 0
+            stepSize: 1.0
+            colorScheme: hifi.colorSchemes.dark
+            value: 33.0
+
+            onEditingFinished: {
+                sendConfigurationSettings();
+            }
+        }
+
+        HifiControls.SpinBox {
+            id: shoulderWidth
+            width: 160
+            label: "Shoulder Width"
+            suffix: " cm"
+            minimumValue: 0
+            stepSize: 1.0
+            decimals: 1
+            colorScheme: hifi.colorSchemes.dark
+            value: 48
+
+            onEditingFinished: {
+                sendConfigurationSettings();
+            }
+        }
+    }
+
     Separator {
         id: bottomSeperator
         width: parent.width
-        anchors.top: shoulderConfig.bottom
-        anchors.topMargin: 10
+        anchors.top: shoulderAdditionalConfig.visible ? shoulderAdditionalConfig.bottom : shoulderConfig.bottom
+        anchors.topMargin: (shoulderAdditionalConfig.visible ? 25 : 10)
     }
 
-
-
     Rectangle {
         id: calibrationButton
         property int color: hifi.buttons.blue
@@ -835,6 +878,9 @@ Rectangle {
         var viveController = settings["handController"];
         var desktopMode = settings["desktopMode"];
 
+        armCircumference.value = settings.armCircumference;
+        shoulderWidth.value = settings.shoulderWidth;
+
         if (HmdHead) {
             headBox.checked = true;
             headPuckBox.checked = false;
@@ -1010,6 +1056,8 @@ Rectangle {
             "bodyConfiguration": trackerConfiguration,
             "headConfiguration": headObject,
             "handConfiguration": handObject,
+            "armCircumference": armCircumference.value,
+            "shoulderWidth": shoulderWidth.value,
             "desktopMode": viveInDesktop.checked
         }
 
diff --git a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml
index 2fd33e9cbc..9076cd6c48 100644
--- a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml
+++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml
@@ -114,6 +114,7 @@ Item {
         }
 
         function clearMenus() {
+            topMenu = null
             d.clear()
         }
 
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 5e2b7f638a..bc2e2f8ec8 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -231,6 +231,7 @@ static const QString FBX_EXTENSION  = ".fbx";
 static const QString OBJ_EXTENSION  = ".obj";
 static const QString AVA_JSON_EXTENSION = ".ava.json";
 static const QString WEB_VIEW_TAG = "noDownload=true";
+static const QString ZIP_EXTENSION = ".zip";
 
 static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f;
 
@@ -262,7 +263,8 @@ const QHash<QString, Application::AcceptURLMethod> Application::_acceptedExtensi
     { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl },
     { JSON_EXTENSION, &Application::importJSONFromURL },
     { JS_EXTENSION, &Application::askToLoadScript },
-    { FST_EXTENSION, &Application::askToSetAvatarUrl }
+    { FST_EXTENSION, &Application::askToSetAvatarUrl },
+    { ZIP_EXTENSION, &Application::importFromZIP }
 };
 
 class DeadlockWatchdogThread : public QThread {
@@ -2262,7 +2264,7 @@ void Application::paintGL() {
             QMutexLocker viewLocker(&_viewMutex);
             _viewFrustum.calculate();
         }
-        renderArgs = RenderArgs(_gpuContext, getEntities(), lodManager->getOctreeSizeScale(),
+        renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(),
             lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE,
             RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE);
         {
@@ -2792,6 +2794,20 @@ bool Application::importSVOFromURL(const QString& urlString) {
     return true;
 }
 
+void Application::onPresent(quint32 frameCount) {
+    if (shouldPaint()) {
+        postEvent(this, new QEvent(static_cast<QEvent::Type>(Idle)), Qt::HighEventPriority);
+        postEvent(this, new QEvent(static_cast<QEvent::Type>(Paint)), Qt::HighEventPriority);
+    }
+}
+
+bool Application::importFromZIP(const QString& filePath) {
+    qDebug() << "A zip file has been dropped in: " << filePath;
+    QUrl empty;
+    qApp->getFileDownloadInterface()->runUnzip(filePath, empty, true, true);
+    return true;
+}
+
 bool _renderRequested { false };
 
 bool Application::event(QEvent* event) {
@@ -2808,23 +2824,9 @@ bool Application::event(QEvent* event) {
         // Explicit idle keeps the idle running at a lower interval, but without any rendering
         // see (windowMinimizedChanged)
         case Event::Idle:
-            {
-                float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed();
-                _lastTimeUpdated.start();
-                idle(nsecsElapsed);
-            }
-            return true;
-
-        case Event::Present:
-            if (!_renderRequested) {
-                float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed();
-                if (shouldPaint(nsecsElapsed)) {
-                    _renderRequested = true;
-                    _lastTimeUpdated.start();
-                    idle(nsecsElapsed);
-                    postEvent(this, new QEvent(static_cast<QEvent::Type>(Paint)), Qt::HighEventPriority);
-                }
-            }
+            idle();
+            // Clear the event queue of pending idle calls
+            removePostedEvents(this, Idle);
             return true;
 
         case Event::Paint:
@@ -2832,9 +2834,8 @@ bool Application::event(QEvent* event) {
             //       or AvatarInputs will mysteriously move to the bottom-right
             AvatarInputs::getInstance()->update();
             paintGL();
-            // wait for the next present event before starting idle / paint again
-            removePostedEvents(this, Present);
-            _renderRequested = false;
+            // Clear the event queue of pending paint calls
+            removePostedEvents(this, Paint);
             return true;
 
         default:
@@ -3653,7 +3654,7 @@ bool Application::acceptSnapshot(const QString& urlString) {
 
 static uint32_t _renderedFrameIndex { INVALID_FRAME };
 
-bool Application::shouldPaint(float nsecsElapsed) {
+bool Application::shouldPaint() {
     if (_aboutToQuit) {
         return false;
     }
@@ -3673,11 +3674,9 @@ bool Application::shouldPaint(float nsecsElapsed) {
             (float)paintDelaySamples / paintDelayUsecs << "us";
     }
 #endif
-
-    float msecondsSinceLastUpdate = nsecsElapsed / NSECS_PER_USEC / USECS_PER_MSEC;
-
+    
     // Throttle if requested
-    if (displayPlugin->isThrottled() && (msecondsSinceLastUpdate < THROTTLED_SIM_FRAME_PERIOD_MS)) {
+    if (displayPlugin->isThrottled() && (_lastTimeUpdated.elapsed() < THROTTLED_SIM_FRAME_PERIOD_MS)) {
         return false;
     }
 
@@ -3894,7 +3893,7 @@ void setupCpuMonitorThread() {
 #endif
 
 
-void Application::idle(float nsecsElapsed) {
+void Application::idle() {
     PerformanceTimer perfTimer("idle");
 
     // Update the deadlock watchdog
@@ -3951,7 +3950,8 @@ void Application::idle(float nsecsElapsed) {
         steamClient->runCallbacks();
     }
 
-    float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND;
+    float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND;
+    _lastTimeUpdated.start();
 
     // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus.
     if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) {
@@ -4185,6 +4185,7 @@ void Application::loadSettings() {
     //DependencyManager::get<LODManager>()->setAutomaticLODAdjust(false);
 
     Menu::getInstance()->loadSettings();
+
     // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it.
     auto pluginManager = PluginManager::getInstance();
     auto plugins = pluginManager->getPreferredDisplayPlugins();
@@ -4198,24 +4199,44 @@ void Application::loadSettings() {
                 break;
             }
         }
-    } else {
+    }
+
+    Setting::Handle<bool> firstRun { Settings::firstRun, true };
+    bool isFirstPerson = false;
+    if (firstRun.get()) {
         // If this is our first run, and no preferred devices were set, default to
         // an HMD device if available.
-        Setting::Handle<bool> firstRun { Settings::firstRun, true };
-        if (firstRun.get()) {
-            auto displayPlugins = pluginManager->getDisplayPlugins();
-            for (auto& plugin : displayPlugins) {
-                if (plugin->isHmd()) {
-                    if (auto action = menu->getActionForOption(plugin->getName())) {
-                        action->setChecked(true);
-                        action->trigger();
-                        break;
-                    }
+        auto displayPlugins = pluginManager->getDisplayPlugins();
+        for (auto& plugin : displayPlugins) {
+            if (plugin->isHmd()) {
+                if (auto action = menu->getActionForOption(plugin->getName())) {
+                    action->setChecked(true);
+                    action->trigger();
+                    break;
                 }
             }
         }
+
+        isFirstPerson = (qApp->isHMDMode());
+    } else {
+        // if this is not the first run, the camera will be initialized differently depending on user settings
+
+        if (qApp->isHMDMode()) {
+            // if the HMD is active, use first-person camera, unless the appropriate setting is checked
+            isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD);
+        } else {
+            // if HMD is not active, only use first person if the menu option is checked
+            isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson);
+        }
     }
 
+    // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings
+    // dictated that we should be in first person
+    Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson);
+    Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson);
+    _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON);
+    cameraMenuChanged();
+
     auto inputs = pluginManager->getInputPlugins();
     for (auto plugin : inputs) {
         if (!plugin->isActive()) {
@@ -4264,7 +4285,6 @@ void Application::init() {
     DependencyManager::get<DeferredLightingEffect>()->init();
 
     DependencyManager::get<AvatarManager>()->init();
-    _myCamera.setMode(CAMERA_MODE_FIRST_PERSON);
 
     _timerStart.start();
     _lastTimeUpdated.start();
@@ -4416,10 +4436,9 @@ void Application::updateMyAvatarLookAtPosition() {
             }
         } else {
             //  I am not looking at anyone else, so just look forward
-            auto headPose = myAvatar->getHeadControllerPoseInSensorFrame();
+            auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD);
             if (headPose.isValid()) {
-                glm::mat4 worldHeadMat = myAvatar->getSensorToWorldMatrix() * headPose.getMatrix();
-                lookAtSpot = transformPoint(worldHeadMat, glm::vec3(0.0f, 0.0f, TREE_SCALE));
+                lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE));
             } else {
                 lookAtSpot = myAvatar->getHead()->getEyePosition() +
                     (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE));
@@ -4833,52 +4852,76 @@ void Application::update(float deltaTime) {
         myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z));
     }
 
-    controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND);
-    controller::Pose rightHandPose = userInputMapper->getPoseState(controller::Action::RIGHT_HAND);
-    auto myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition());
-    auto worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix());
-    auto avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix;
-    myAvatar->setHandControllerPosesInSensorFrame(leftHandPose.transform(avatarToSensorMatrix), rightHandPose.transform(avatarToSensorMatrix));
+    static const std::vector<controller::Action> avatarControllerActions = {
+        controller::Action::LEFT_HAND,
+        controller::Action::RIGHT_HAND,
+        controller::Action::LEFT_FOOT,
+        controller::Action::RIGHT_FOOT,
+        controller::Action::HIPS,
+        controller::Action::SPINE2,
+        controller::Action::HEAD,
+        controller::Action::LEFT_HAND_THUMB1,
+        controller::Action::LEFT_HAND_THUMB2,
+        controller::Action::LEFT_HAND_THUMB3,
+        controller::Action::LEFT_HAND_THUMB4,
+        controller::Action::LEFT_HAND_INDEX1,
+        controller::Action::LEFT_HAND_INDEX2,
+        controller::Action::LEFT_HAND_INDEX3,
+        controller::Action::LEFT_HAND_INDEX4,
+        controller::Action::LEFT_HAND_MIDDLE1,
+        controller::Action::LEFT_HAND_MIDDLE2,
+        controller::Action::LEFT_HAND_MIDDLE3,
+        controller::Action::LEFT_HAND_MIDDLE4,
+        controller::Action::LEFT_HAND_RING1,
+        controller::Action::LEFT_HAND_RING2,
+        controller::Action::LEFT_HAND_RING3,
+        controller::Action::LEFT_HAND_RING4,
+        controller::Action::LEFT_HAND_PINKY1,
+        controller::Action::LEFT_HAND_PINKY2,
+        controller::Action::LEFT_HAND_PINKY3,
+        controller::Action::LEFT_HAND_PINKY4,
+        controller::Action::RIGHT_HAND_THUMB1,
+        controller::Action::RIGHT_HAND_THUMB2,
+        controller::Action::RIGHT_HAND_THUMB3,
+        controller::Action::RIGHT_HAND_THUMB4,
+        controller::Action::RIGHT_HAND_INDEX1,
+        controller::Action::RIGHT_HAND_INDEX2,
+        controller::Action::RIGHT_HAND_INDEX3,
+        controller::Action::RIGHT_HAND_INDEX4,
+        controller::Action::RIGHT_HAND_MIDDLE1,
+        controller::Action::RIGHT_HAND_MIDDLE2,
+        controller::Action::RIGHT_HAND_MIDDLE3,
+        controller::Action::RIGHT_HAND_MIDDLE4,
+        controller::Action::RIGHT_HAND_RING1,
+        controller::Action::RIGHT_HAND_RING2,
+        controller::Action::RIGHT_HAND_RING3,
+        controller::Action::RIGHT_HAND_RING4,
+        controller::Action::RIGHT_HAND_PINKY1,
+        controller::Action::RIGHT_HAND_PINKY2,
+        controller::Action::RIGHT_HAND_PINKY3,
+        controller::Action::RIGHT_HAND_PINKY4,
+        controller::Action::LEFT_ARM,
+        controller::Action::RIGHT_ARM,
+        controller::Action::LEFT_SHOULDER,
+        controller::Action::RIGHT_SHOULDER,
+        controller::Action::LEFT_FORE_ARM,
+        controller::Action::RIGHT_FORE_ARM,
+        controller::Action::LEFT_LEG,
+        controller::Action::RIGHT_LEG,
+        controller::Action::LEFT_UP_LEG,
+        controller::Action::RIGHT_UP_LEG,
+        controller::Action::LEFT_TOE_BASE,
+        controller::Action::RIGHT_TOE_BASE
+    };
 
-    // If have previously done finger poses or there are new valid finger poses, update finger pose values. This so that if
-    // fingers are not being controlled, finger joints are not updated in MySkeletonModel.
-    // Assumption: Finger poses are either all present and valid or not present at all; thus can test just one joint.
-    MyAvatar::FingerPosesMap leftHandFingerPoses;
-    if (myAvatar->getLeftHandFingerControllerPosesInSensorFrame().size() > 0
-            || userInputMapper->getPoseState(controller::Action::LEFT_HAND_THUMB1).isValid()) {
-        for (int i = (int)controller::Action::LEFT_HAND_THUMB1; i <= (int)controller::Action::LEFT_HAND_PINKY4; i++) {
-            leftHandFingerPoses[i] = {
-                userInputMapper->getPoseState((controller::Action)i).transform(avatarToSensorMatrix),
-                userInputMapper->getActionName((controller::Action)i)
-            };
-        }
+    // copy controller poses from userInputMapper to myAvatar.
+    glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition());
+    glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix());
+    glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix;
+    for (auto& action : avatarControllerActions) {
+        controller::Pose pose = userInputMapper->getPoseState(action);
+        myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix));
     }
-    MyAvatar::FingerPosesMap rightHandFingerPoses;
-    if (myAvatar->getRightHandFingerControllerPosesInSensorFrame().size() > 0
-        || userInputMapper->getPoseState(controller::Action::RIGHT_HAND_THUMB1).isValid()) {
-        for (int i = (int)controller::Action::RIGHT_HAND_THUMB1; i <= (int)controller::Action::RIGHT_HAND_PINKY4; i++) {
-            rightHandFingerPoses[i] = {
-                userInputMapper->getPoseState((controller::Action)i).transform(avatarToSensorMatrix),
-                userInputMapper->getActionName((controller::Action)i)
-            };
-        }
-    }
-    myAvatar->setFingerControllerPosesInSensorFrame(leftHandFingerPoses, rightHandFingerPoses);
-
-    controller::Pose leftFootPose = userInputMapper->getPoseState(controller::Action::LEFT_FOOT);
-    controller::Pose rightFootPose = userInputMapper->getPoseState(controller::Action::RIGHT_FOOT);
-    myAvatar->setFootControllerPosesInSensorFrame(leftFootPose.transform(avatarToSensorMatrix), rightFootPose.transform(avatarToSensorMatrix));
-
-    controller::Pose hipsPose = userInputMapper->getPoseState(controller::Action::HIPS);
-    controller::Pose spine2Pose = userInputMapper->getPoseState(controller::Action::SPINE2);
-    myAvatar->setSpineControllerPosesInSensorFrame(hipsPose.transform(avatarToSensorMatrix), spine2Pose.transform(avatarToSensorMatrix));
-
-    controller::Pose headPose = userInputMapper->getPoseState(controller::Action::HEAD);
-    myAvatar->setHeadControllerPoseInSensorFrame(headPose.transform(avatarToSensorMatrix));
-
-    controller::Pose leftArmPose = userInputMapper->getPoseState(controller::Action::LEFT_ARM);
-    controller::Pose rightArmPose = userInputMapper->getPoseState(controller::Action::RIGHT_ARM);
-    myAvatar->setArmControllerPosesInSensorFrame(leftArmPose.transform(avatarToSensorMatrix), rightArmPose.transform(avatarToSensorMatrix));
 
     updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process...
     updateDialogs(deltaTime); // update various stats dialogs if present
@@ -6242,7 +6285,7 @@ void Application::addAssetToWorldFromURLRequestFinished() {
             if (tempFile.open(QIODevice::WriteOnly)) {
                 tempFile.write(request->getData());
                 addAssetToWorldInfoClear(filename);  // Remove message from list; next one added will have a different key.
-                qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true);
+                qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true, false);
             } else {
                 QString errorInfo = "Couldn't open temporary file for download";
                 qWarning(interfaceapp) << errorInfo;
@@ -6272,12 +6315,18 @@ void Application::addAssetToWorldUnzipFailure(QString filePath) {
     addAssetToWorldError(filename, "Couldn't unzip file " + filename + ".");
 }
 
-void Application::addAssetToWorld(QString filePath) {
+void Application::addAssetToWorld(QString filePath, QString zipFile, bool isZip) {
     // Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget().
-
-    QString path = QUrl(filePath).toLocalFile();
+    QString mapping;
+    QString path = filePath;
     QString filename = filenameFromPath(path);
-    QString mapping = "/" + filename;
+    if (isZip) {
+        QString assetFolder = zipFile.section("/", -1);
+        assetFolder.remove(".zip");
+        mapping = "/" + assetFolder + "/" + filename;
+    } else {
+        mapping = "/" + filename;
+    }
 
     // Test repeated because possibly different code paths.
     if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
@@ -6358,7 +6407,13 @@ void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, Q
             qWarning(interfaceapp) << "Error downloading model: " + errorInfo;
             addAssetToWorldError(filenameFromPath(filePath), errorInfo);
         } else {
-            addAssetToWorldAddEntity(filePath, mapping);
+            // to prevent files that aren't models from being loaded into world automatically
+            if (filePath.endsWith(".obj") || filePath.endsWith(".fbx")) {
+                addAssetToWorldAddEntity(filePath, mapping);
+            } else {
+                qCDebug(interfaceapp) << "Zipped contents are not supported entity files";
+                addAssetToWorldInfoDone(filenameFromPath(filePath));
+            }
         }
         request->deleteLater();
     });
@@ -6648,15 +6703,18 @@ void Application::onAssetToWorldMessageBoxClosed() {
 }
 
 
-void Application::handleUnzip(QString zipFile, QString unzipFile, bool autoAdd) {
+void Application::handleUnzip(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip) {
     if (autoAdd) {
         if (!unzipFile.isEmpty()) {
-            addAssetToWorld(unzipFile);
+            for (int i = 0; i < unzipFile.length(); i++) {
+                qCDebug(interfaceapp) << "Preparing file for asset server: " << unzipFile.at(i);
+                addAssetToWorld(unzipFile.at(i), zipFile, isZip);
+            }
         } else {
             addAssetToWorldUnzipFailure(zipFile);
         }
     } else {
-        showAssetServerWidget(unzipFile);
+        showAssetServerWidget(unzipFile.first());
     }
 }
 
@@ -7119,6 +7177,7 @@ void Application::updateDisplayMode() {
 
         auto oldDisplayPlugin = _displayPlugin;
         if (_displayPlugin) {
+            disconnect(_displayPluginPresentConnection);
             _displayPlugin->deactivate();
         }
 
@@ -7159,6 +7218,7 @@ void Application::updateDisplayMode() {
         _offscreenContext->makeCurrent();
         getApplicationCompositor().setDisplayPlugin(newDisplayPlugin);
         _displayPlugin = newDisplayPlugin;
+        _displayPluginPresentConnection = connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent);
         offscreenUi->getDesktop()->setProperty("repositionLocked", wasRepositionLocked);
     }
 
diff --git a/interface/src/Application.h b/interface/src/Application.h
index 123aa85e2e..f8eb393f9e 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -129,8 +129,7 @@ public:
     virtual DisplayPluginPointer getActiveDisplayPlugin() const override;
 
     enum Event {
-        Present = DisplayPlugin::Present,
-        Paint,
+        Paint = QEvent::User + 1,
         Idle,
         Lambda
     };
@@ -332,14 +331,14 @@ public slots:
     // FIXME: Move addAssetToWorld* methods to own class?
     void addAssetToWorldFromURL(QString url);
     void addAssetToWorldFromURLRequestFinished();
-    void addAssetToWorld(QString filePath);
+    void addAssetToWorld(QString filePath, QString zipFile, bool isZip);
     void addAssetToWorldUnzipFailure(QString filePath);
     void addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy);
     void addAssetToWorldUpload(QString filePath, QString mapping);
     void addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash);
     void addAssetToWorldAddEntity(QString filePath, QString mapping);
 
-    void handleUnzip(QString sourceFile, QString destinationFile, bool autoAdd);
+    void handleUnzip(QString sourceFile, QStringList destinationFile, bool autoAdd, bool isZip);
 
     FileScriptingInterface* getFileDownloadInterface() { return _fileDownload; }
 
@@ -409,6 +408,7 @@ private slots:
     void clearDomainOctreeDetails();
     void clearDomainAvatars();
     void onAboutToQuit();
+    void onPresent(quint32 frameCount);
 
     void resettingDomain();
 
@@ -455,8 +455,8 @@ private:
 
     void cleanupBeforeQuit();
 
-    bool shouldPaint(float nsecsElapsed);
-    void idle(float nsecsElapsed);
+    bool shouldPaint();
+    void idle();
     void update(float deltaTime);
 
     // Various helper functions called during update()
@@ -481,6 +481,7 @@ private:
 
     bool importJSONFromURL(const QString& urlString);
     bool importSVOFromURL(const QString& urlString);
+    bool importFromZIP(const QString& filePath);
 
     bool nearbyEntitiesAreReadyForPhysics();
     int processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode);
@@ -518,6 +519,7 @@ private:
 
     OffscreenGLCanvas* _offscreenContext { nullptr };
     DisplayPluginPointer _displayPlugin;
+    QMetaObject::Connection _displayPluginPresentConnection;
     mutable std::mutex _displayPluginLock;
     InputPluginList _activeInputPlugins;
 
diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp
index c1d2f903f3..fe5355ff2e 100644
--- a/interface/src/avatar/AvatarActionHold.cpp
+++ b/interface/src/avatar/AvatarActionHold.cpp
@@ -134,9 +134,9 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::
             // fetch the hand controller pose
             controller::Pose pose;
             if (isRightHand) {
-                pose = myAvatar->getRightHandControllerPoseInWorldFrame();
+                pose = myAvatar->getControllerPoseInWorldFrame(controller::Action::RIGHT_HAND);
             } else {
-                pose = myAvatar->getLeftHandControllerPoseInWorldFrame();
+                pose = myAvatar->getControllerPoseInWorldFrame(controller::Action::LEFT_HAND);
             }
 
             if (pose.isValid()) {
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index 474aa48e63..36295e87b8 100755
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -429,7 +429,7 @@ void MyAvatar::update(float deltaTime) {
     }
 
 #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE
-    glm::vec3 p = transformPoint(getSensorToWorldMatrix(), getHeadControllerPoseInAvatarFrame() *
+    glm::vec3 p = transformPoint(getSensorToWorldMatrix(), getControllerPoseInAvatarFrame(controller::Pose::HEAD) *
                                  glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y));
     DebugDraw::getInstance().addMarker("facing-avg", getOrientation(), p, glm::vec4(1.0f));
     p = transformPoint(getSensorToWorldMatrix(), getHMDSensorPosition() +
@@ -664,7 +664,7 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) {
 
     _hmdSensorPosition = newHmdSensorPosition;
     _hmdSensorOrientation = glm::quat_cast(hmdSensorMatrix);
-    auto headPose = _headControllerPoseInSensorFrameCache.get();
+    auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD);
     if (headPose.isValid()) {
         _headControllerFacing = getFacingDir2D(headPose.rotation);
     } else {
@@ -760,37 +760,37 @@ void MyAvatar::updateFromTrackers(float deltaTime) {
 }
 
 glm::vec3 MyAvatar::getLeftHandPosition() const {
-    auto pose = getLeftHandControllerPoseInAvatarFrame();
+    auto pose = getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND);
     return pose.isValid() ? pose.getTranslation() : glm::vec3(0.0f);
 }
 
 glm::vec3 MyAvatar::getRightHandPosition() const {
-    auto pose = getRightHandControllerPoseInAvatarFrame();
+    auto pose = getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND);
     return pose.isValid() ? pose.getTranslation() : glm::vec3(0.0f);
 }
 
 glm::vec3 MyAvatar::getLeftHandTipPosition() const {
     const float TIP_LENGTH = 0.3f;
-    auto pose = getLeftHandControllerPoseInAvatarFrame();
+    auto pose = getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND);
     return pose.isValid() ? pose.getTranslation() * pose.getRotation() + glm::vec3(0.0f, TIP_LENGTH, 0.0f) : glm::vec3(0.0f);
 }
 
 glm::vec3 MyAvatar::getRightHandTipPosition() const {
     const float TIP_LENGTH = 0.3f;
-    auto pose = getRightHandControllerPoseInAvatarFrame();
+    auto pose = getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND);
     return pose.isValid() ? pose.getTranslation() * pose.getRotation() + glm::vec3(0.0f, TIP_LENGTH, 0.0f) : glm::vec3(0.0f);
 }
 
 controller::Pose MyAvatar::getLeftHandPose() const {
-    return getLeftHandControllerPoseInAvatarFrame();
+    return getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND);
 }
 
 controller::Pose MyAvatar::getRightHandPose() const {
-    return getRightHandControllerPoseInAvatarFrame();
+    return getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND);
 }
 
 controller::Pose MyAvatar::getLeftHandTipPose() const {
-    auto pose = getLeftHandControllerPoseInAvatarFrame();
+    auto pose = getLeftHandPose();
     glm::vec3 tipTrans = getLeftHandTipPosition();
     pose.velocity += glm::cross(pose.getAngularVelocity(), pose.getTranslation() - tipTrans);
     pose.translation = tipTrans;
@@ -798,7 +798,7 @@ controller::Pose MyAvatar::getLeftHandTipPose() const {
 }
 
 controller::Pose MyAvatar::getRightHandTipPose() const {
-    auto pose = getRightHandControllerPoseInAvatarFrame();
+    auto pose = getRightHandPose();
     glm::vec3 tipTrans = getRightHandTipPosition();
     pose.velocity += glm::cross(pose.getAngularVelocity(), pose.getTranslation() - tipTrans);
     pose.translation = tipTrans;
@@ -1430,159 +1430,43 @@ void MyAvatar::rebuildCollisionShape() {
     _characterController.setLocalBoundingBox(corner, diagonal);
 }
 
-
-void MyAvatar::setHandControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right) {
-    _leftHandControllerPoseInSensorFrameCache.set(left);
-    _rightHandControllerPoseInSensorFrameCache.set(right);
+void MyAvatar::setControllerPoseInSensorFrame(controller::Action action, const controller::Pose& pose) {
+    std::lock_guard<std::mutex> guard(_controllerPoseMapMutex);
+    auto iter = _controllerPoseMap.find(action);
+    if (iter != _controllerPoseMap.end()) {
+        iter->second = pose;
+    } else {
+        _controllerPoseMap.insert({ action, pose });
+    }
 }
 
-controller::Pose MyAvatar::getLeftHandControllerPoseInSensorFrame() const {
-    return _leftHandControllerPoseInSensorFrameCache.get();
+controller::Pose MyAvatar::getControllerPoseInSensorFrame(controller::Action action) const {
+    std::lock_guard<std::mutex> guard(_controllerPoseMapMutex);
+    auto iter = _controllerPoseMap.find(action);
+    if (iter != _controllerPoseMap.end()) {
+        return iter->second;
+    } else {
+        return controller::Pose(); // invalid pose
+    }
 }
 
-controller::Pose MyAvatar::getRightHandControllerPoseInSensorFrame() const {
-    return _rightHandControllerPoseInSensorFrameCache.get();
+controller::Pose MyAvatar::getControllerPoseInWorldFrame(controller::Action action) const {
+    auto pose = getControllerPoseInSensorFrame(action);
+    if (pose.valid) {
+        return pose.transform(getSensorToWorldMatrix());
+    } else {
+        return controller::Pose(); // invalid pose
+    }
 }
 
-controller::Pose MyAvatar::getLeftHandControllerPoseInWorldFrame() const {
-    return _leftHandControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getRightHandControllerPoseInWorldFrame() const {
-    return _rightHandControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getLeftHandControllerPoseInAvatarFrame() const {
-    glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getLeftHandControllerPoseInWorldFrame().transform(invAvatarMatrix);
-}
-
-controller::Pose MyAvatar::getRightHandControllerPoseInAvatarFrame() const {
-    glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getRightHandControllerPoseInWorldFrame().transform(invAvatarMatrix);
-}
-
-void MyAvatar::setFingerControllerPosesInSensorFrame(const FingerPosesMap& left, const FingerPosesMap& right) {
-    _leftHandFingerPosesInSensorFramceCache.set(left);
-    _rightHandFingerPosesInSensorFramceCache.set(right);
-}
-
-MyAvatar::FingerPosesMap MyAvatar::getLeftHandFingerControllerPosesInSensorFrame() const {
-    return _leftHandFingerPosesInSensorFramceCache.get();
-}
-
-MyAvatar::FingerPosesMap MyAvatar::getRightHandFingerControllerPosesInSensorFrame() const {
-    return _rightHandFingerPosesInSensorFramceCache.get();
-}
-
-void MyAvatar::setFootControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right) {
-    _leftFootControllerPoseInSensorFrameCache.set(left);
-    _rightFootControllerPoseInSensorFrameCache.set(right);
-}
-
-controller::Pose MyAvatar::getLeftFootControllerPoseInSensorFrame() const {
-    return _leftFootControllerPoseInSensorFrameCache.get();
-}
-
-controller::Pose MyAvatar::getRightFootControllerPoseInSensorFrame() const {
-    return _rightFootControllerPoseInSensorFrameCache.get();
-}
-
-controller::Pose MyAvatar::getLeftFootControllerPoseInWorldFrame() const {
-    return _leftFootControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getRightFootControllerPoseInWorldFrame() const {
-    return _rightFootControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getLeftFootControllerPoseInAvatarFrame() const {
-    glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getLeftFootControllerPoseInWorldFrame().transform(invAvatarMatrix);
-}
-
-controller::Pose MyAvatar::getRightFootControllerPoseInAvatarFrame() const {
-    glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getRightFootControllerPoseInWorldFrame().transform(invAvatarMatrix);
-}
-
-void MyAvatar::setSpineControllerPosesInSensorFrame(const controller::Pose& hips, const controller::Pose& spine2) {
-    _hipsControllerPoseInSensorFrameCache.set(hips);
-    _spine2ControllerPoseInSensorFrameCache.set(spine2);
-}
-
-controller::Pose MyAvatar::getHipsControllerPoseInSensorFrame() const {
-    return _hipsControllerPoseInSensorFrameCache.get();
-}
-
-controller::Pose MyAvatar::getSpine2ControllerPoseInSensorFrame() const {
-    return _spine2ControllerPoseInSensorFrameCache.get();
-}
-
-controller::Pose MyAvatar::getHipsControllerPoseInWorldFrame() const {
-    return _hipsControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getSpine2ControllerPoseInWorldFrame() const {
-    return _spine2ControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getHipsControllerPoseInAvatarFrame() const {
-    glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getHipsControllerPoseInWorldFrame().transform(invAvatarMatrix);
-}
-
-controller::Pose MyAvatar::getSpine2ControllerPoseInAvatarFrame() const {
-    glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getSpine2ControllerPoseInWorldFrame().transform(invAvatarMatrix);
-}
-
-void MyAvatar::setHeadControllerPoseInSensorFrame(const controller::Pose& head) {
-    _headControllerPoseInSensorFrameCache.set(head);
-}
-
-controller::Pose MyAvatar::getHeadControllerPoseInSensorFrame() const {
-    return _headControllerPoseInSensorFrameCache.get();
-}
-
-controller::Pose MyAvatar::getHeadControllerPoseInWorldFrame() const {
-    return _headControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getHeadControllerPoseInAvatarFrame() const {
-    glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getHeadControllerPoseInWorldFrame().transform(invAvatarMatrix);
-}
-
-void MyAvatar::setArmControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right) {
-    _leftArmControllerPoseInSensorFrameCache.set(left);
-    _rightArmControllerPoseInSensorFrameCache.set(right);
-}
-
-controller::Pose MyAvatar::getLeftArmControllerPoseInSensorFrame() const {
-    return _leftArmControllerPoseInSensorFrameCache.get();
-}
-
-controller::Pose MyAvatar::getRightArmControllerPoseInSensorFrame() const {
-    return _rightArmControllerPoseInSensorFrameCache.get();
-}
-
-controller::Pose MyAvatar::getLeftArmControllerPoseInWorldFrame() const {
-    return getLeftArmControllerPoseInSensorFrame().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getRightArmControllerPoseInWorldFrame() const {
-    return getRightArmControllerPoseInSensorFrame().transform(getSensorToWorldMatrix());
-}
-
-controller::Pose MyAvatar::getLeftArmControllerPoseInAvatarFrame() const {
-    glm::mat4 worldToAvatarMat = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getLeftArmControllerPoseInWorldFrame().transform(worldToAvatarMat);
-}
-
-controller::Pose MyAvatar::getRightArmControllerPoseInAvatarFrame() const {
-    glm::mat4 worldToAvatarMat = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
-    return getRightArmControllerPoseInWorldFrame().transform(worldToAvatarMat);
+controller::Pose MyAvatar::getControllerPoseInAvatarFrame(controller::Action action) const {
+    auto pose = getControllerPoseInWorldFrame(action);
+    if (pose.valid) {
+        glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition()));
+        return pose.transform(invAvatarMatrix);
+    } else {
+        return controller::Pose(); // invalid pose
+    }
 }
 
 void MyAvatar::updateMotors() {
@@ -1645,7 +1529,7 @@ void MyAvatar::prepareForPhysicsSimulation() {
     _characterController.setParentVelocity(parentVelocity);
 
     _characterController.setPositionAndOrientation(getPosition(), getOrientation());
-    auto headPose = getHeadControllerPoseInAvatarFrame();
+    auto headPose = getControllerPoseInAvatarFrame(controller::Action::HEAD);
     if (headPose.isValid()) {
         _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput());
     } else {
@@ -1899,8 +1783,8 @@ void MyAvatar::postUpdate(float deltaTime) {
     }
 
     if (_enableDebugDrawHandControllers) {
-        auto leftHandPose = getLeftHandControllerPoseInWorldFrame();
-        auto rightHandPose = getRightHandControllerPoseInWorldFrame();
+        auto leftHandPose = getControllerPoseInWorldFrame(controller::Action::LEFT_HAND);
+        auto rightHandPose = getControllerPoseInWorldFrame(controller::Action::RIGHT_HAND);
 
         if (leftHandPose.isValid()) {
             DebugDraw::getInstance().addMarker("leftHandController", leftHandPose.getRotation(), leftHandPose.getTranslation(), glm::vec4(1));
@@ -2053,7 +1937,7 @@ void MyAvatar::updateOrientation(float deltaTime) {
 
     getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime);
 
-    auto headPose = getHeadControllerPoseInAvatarFrame();
+    auto headPose = getControllerPoseInAvatarFrame(controller::Action::HEAD);
     if (headPose.isValid()) {
         glm::quat localOrientation = headPose.rotation * Quaternions::Y_180;
         // these angles will be in radians
@@ -2689,10 +2573,10 @@ bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const {
 glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const {
     glm::vec3 headPosition;
     glm::quat headOrientation;
-    auto headPose = getHeadControllerPoseInSensorFrame();
+    auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD);
     if (headPose.isValid()) {
-        headPosition = getHeadControllerPoseInSensorFrame().translation;
-        headOrientation = getHeadControllerPoseInSensorFrame().rotation * Quaternions::Y_180;
+        headPosition = headPose.translation;
+        headOrientation = headPose.rotation * Quaternions::Y_180;
     }
     const glm::quat headOrientationYawOnly = cancelOutRollAndPitch(headOrientation);
 
@@ -3002,19 +2886,19 @@ glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const {
 
     switch (index) {
         case CONTROLLER_LEFTHAND_INDEX: {
-            return getLeftHandControllerPoseInAvatarFrame().getRotation();
+            return getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).getRotation();
         }
         case CONTROLLER_RIGHTHAND_INDEX: {
-            return getRightHandControllerPoseInAvatarFrame().getRotation();
+            return getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).getRotation();
         }
         case CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX: {
-            auto pose = _leftHandControllerPoseInSensorFrameCache.get();
+            auto pose = getControllerPoseInSensorFrame(controller::Action::LEFT_HAND);
             glm::mat4 controllerSensorMatrix = createMatFromQuatAndPos(pose.rotation, pose.translation);
             glm::mat4 result = computeCameraRelativeHandControllerMatrix(controllerSensorMatrix);
             return glmExtractRotation(result);
         }
         case CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX: {
-            auto pose = _rightHandControllerPoseInSensorFrameCache.get();
+            auto pose = getControllerPoseInSensorFrame(controller::Action::RIGHT_HAND);
             glm::mat4 controllerSensorMatrix = createMatFromQuatAndPos(pose.rotation, pose.translation);
             glm::mat4 result = computeCameraRelativeHandControllerMatrix(controllerSensorMatrix);
             return glmExtractRotation(result);
@@ -3039,19 +2923,19 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const {
 
     switch (index) {
         case CONTROLLER_LEFTHAND_INDEX: {
-            return getLeftHandControllerPoseInAvatarFrame().getTranslation();
+            return getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).getTranslation();
         }
         case CONTROLLER_RIGHTHAND_INDEX: {
-            return getRightHandControllerPoseInAvatarFrame().getTranslation();
+            return getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND).getTranslation();
         }
         case CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX: {
-            auto pose = _leftHandControllerPoseInSensorFrameCache.get();
+            auto pose = getControllerPoseInSensorFrame(controller::Action::LEFT_HAND);
             glm::mat4 controllerSensorMatrix = createMatFromQuatAndPos(pose.rotation, pose.translation);
             glm::mat4 result = computeCameraRelativeHandControllerMatrix(controllerSensorMatrix);
             return extractTranslation(result);
         }
         case CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX: {
-            auto pose = _rightHandControllerPoseInSensorFrameCache.get();
+            auto pose = getControllerPoseInSensorFrame(controller::Action::RIGHT_HAND);
             glm::mat4 controllerSensorMatrix = createMatFromQuatAndPos(pose.rotation, pose.translation);
             glm::mat4 result = computeCameraRelativeHandControllerMatrix(controllerSensorMatrix);
             return extractTranslation(result);
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index fa5ffc4b93..dc4357be52 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -473,49 +473,12 @@ public:
 
     virtual void rebuildCollisionShape() override;
 
-    void setHandControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right);
-    controller::Pose getLeftHandControllerPoseInSensorFrame() const;
-    controller::Pose getRightHandControllerPoseInSensorFrame() const;
-    controller::Pose getLeftHandControllerPoseInWorldFrame() const;
-    controller::Pose getRightHandControllerPoseInWorldFrame() const;
-    controller::Pose getLeftHandControllerPoseInAvatarFrame() const;
-    controller::Pose getRightHandControllerPoseInAvatarFrame() const;
-
-    typedef std::map<int, std::pair<controller::Pose, QString>> FingerPosesMap;
-    void setFingerControllerPosesInSensorFrame(const FingerPosesMap& left, const FingerPosesMap& right);
-    FingerPosesMap getLeftHandFingerControllerPosesInSensorFrame() const;
-    FingerPosesMap getRightHandFingerControllerPosesInSensorFrame() const;
-
-    void setFootControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right);
-    controller::Pose getLeftFootControllerPoseInSensorFrame() const;
-    controller::Pose getRightFootControllerPoseInSensorFrame() const;
-    controller::Pose getLeftFootControllerPoseInWorldFrame() const;
-    controller::Pose getRightFootControllerPoseInWorldFrame() const;
-    controller::Pose getLeftFootControllerPoseInAvatarFrame() const;
-    controller::Pose getRightFootControllerPoseInAvatarFrame() const;
-
-    void setSpineControllerPosesInSensorFrame(const controller::Pose& hips, const controller::Pose& spine2);
-    controller::Pose getHipsControllerPoseInSensorFrame() const;
-    controller::Pose getSpine2ControllerPoseInSensorFrame() const;
-    controller::Pose getHipsControllerPoseInWorldFrame() const;
-    controller::Pose getSpine2ControllerPoseInWorldFrame() const;
-    controller::Pose getHipsControllerPoseInAvatarFrame() const;
-    controller::Pose getSpine2ControllerPoseInAvatarFrame() const;
-
-    void setHeadControllerPoseInSensorFrame(const controller::Pose& head);
-    controller::Pose getHeadControllerPoseInSensorFrame() const;
-    controller::Pose getHeadControllerPoseInWorldFrame() const;
-    controller::Pose getHeadControllerPoseInAvatarFrame() const;
     const glm::vec2& getHeadControllerFacingMovingAverage() const { return _headControllerFacingMovingAverage; }
 
-
-    void setArmControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right);
-    controller::Pose getLeftArmControllerPoseInSensorFrame() const;
-    controller::Pose getRightArmControllerPoseInSensorFrame() const;
-    controller::Pose getLeftArmControllerPoseInWorldFrame() const;
-    controller::Pose getRightArmControllerPoseInWorldFrame() const;
-    controller::Pose getLeftArmControllerPoseInAvatarFrame() const;
-    controller::Pose getRightArmControllerPoseInAvatarFrame() const;
+    void setControllerPoseInSensorFrame(controller::Action action, const controller::Pose& pose);
+    controller::Pose getControllerPoseInSensorFrame(controller::Action action) const;
+    controller::Pose getControllerPoseInWorldFrame(controller::Action action) const;
+    controller::Pose getControllerPoseInAvatarFrame(controller::Action action) const;
 
     bool hasDriveInput() const;
 
@@ -804,18 +767,9 @@ private:
     bool _hoverReferenceCameraFacingIsCaptured { false };
     glm::vec3 _hoverReferenceCameraFacing { 0.0f, 0.0f, -1.0f }; // hmd sensor space
 
-    // These are stored in SENSOR frame
-    ThreadSafeValueCache<controller::Pose> _leftHandControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<controller::Pose> _rightHandControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<FingerPosesMap> _leftHandFingerPosesInSensorFramceCache { };
-    ThreadSafeValueCache<FingerPosesMap> _rightHandFingerPosesInSensorFramceCache { };
-    ThreadSafeValueCache<controller::Pose> _leftFootControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<controller::Pose> _rightFootControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<controller::Pose> _hipsControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<controller::Pose> _spine2ControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<controller::Pose> _headControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<controller::Pose> _leftArmControllerPoseInSensorFrameCache { controller::Pose() };
-    ThreadSafeValueCache<controller::Pose> _rightArmControllerPoseInSensorFrameCache { controller::Pose() };
+    // all poses are in sensor-frame
+    std::map<controller::Action, controller::Pose> _controllerPoseMap;
+    mutable std::mutex _controllerPoseMapMutex;
 
     bool _hmdLeanRecenterEnabled = true;
     AnimPose _prePhysicsRoomPose;
diff --git a/interface/src/avatar/MyCharacterController.cpp b/interface/src/avatar/MyCharacterController.cpp
index 3d98a0e604..761e2e5bff 100755
--- a/interface/src/avatar/MyCharacterController.cpp
+++ b/interface/src/avatar/MyCharacterController.cpp
@@ -36,7 +36,7 @@ MyCharacterController::~MyCharacterController() {
 
 void MyCharacterController::setDynamicsWorld(btDynamicsWorld* world) {
     CharacterController::setDynamicsWorld(world);
-    if (world) {
+    if (world && _rigidBody) {
         initRayShotgun(world);
     }
 }
diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp
index 7fc6b9fa26..7fc1850bb2 100644
--- a/interface/src/avatar/MyHead.cpp
+++ b/interface/src/avatar/MyHead.cpp
@@ -34,7 +34,7 @@ glm::quat MyHead::getHeadOrientation() const {
     // always the same.
 
     MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
-    auto headPose = myAvatar->getHeadControllerPoseInWorldFrame();
+    auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD);
     if (headPose.isValid()) {
         return headPose.rotation * Quaternions::Y_180;
     }
diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp
index 97309d9678..6d468c3f30 100644
--- a/interface/src/avatar/MySkeletonModel.cpp
+++ b/interface/src/avatar/MySkeletonModel.cpp
@@ -46,105 +46,82 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
     }
 
     MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
+    assert(myAvatar);
 
     Rig::ControllerParameters params;
 
     AnimPose avatarToRigPose(glm::vec3(1.0f), Quaternions::Y_180, glm::vec3(0.0f));
 
     // input action is the highest priority source for head orientation.
-    auto avatarHeadPose = myAvatar->getHeadControllerPoseInAvatarFrame();
+    auto avatarHeadPose = myAvatar->getControllerPoseInAvatarFrame(controller::Action::HEAD);
     if (avatarHeadPose.isValid()) {
         AnimPose pose(avatarHeadPose.getRotation(), avatarHeadPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_Head] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_Head] = true;
+        params.primaryControllerPoses[Rig::PrimaryControllerType_Head] = avatarToRigPose * pose;
+        params.primaryControllerActiveFlags[Rig::PrimaryControllerType_Head] = true;
     } else {
         // even though full head IK is disabled, the rig still needs the head orientation to rotate the head up and
         // down in desktop mode.
         // preMult 180 is necessary to convert from avatar to rig coordinates.
         // postMult 180 is necessary to convert head from -z forward to z forward.
         glm::quat headRot = Quaternions::Y_180 * head->getFinalOrientationInLocalFrame() * Quaternions::Y_180;
-        params.controllerPoses[Rig::ControllerType_Head] = AnimPose(glm::vec3(1.0f), headRot, glm::vec3(0.0f));
-        params.controllerActiveFlags[Rig::ControllerType_Head] = false;
+        params.primaryControllerPoses[Rig::PrimaryControllerType_Head] = AnimPose(glm::vec3(1.0f), headRot, glm::vec3(0.0f));
+        params.primaryControllerActiveFlags[Rig::PrimaryControllerType_Head] = false;
     }
 
-    auto avatarHipsPose = myAvatar->getHipsControllerPoseInAvatarFrame();
-    if (avatarHipsPose.isValid()) {
-        AnimPose pose(avatarHipsPose.getRotation(), avatarHipsPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_Hips] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_Hips] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_Hips] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_Hips] = false;
+    //
+    // primary controller poses, control IK targets directly.
+    //
+
+    static const std::vector<std::pair<controller::Action, Rig::PrimaryControllerType>> primaryControllers = {
+        { controller::Action::LEFT_HAND, Rig::PrimaryControllerType_LeftHand },
+        { controller::Action::RIGHT_HAND, Rig::PrimaryControllerType_RightHand },
+        { controller::Action::HIPS, Rig::PrimaryControllerType_Hips },
+        { controller::Action::LEFT_FOOT, Rig::PrimaryControllerType_LeftFoot },
+        { controller::Action::RIGHT_FOOT, Rig::PrimaryControllerType_RightFoot },
+        { controller::Action::SPINE2, Rig::PrimaryControllerType_Spine2 }
+    };
+
+    for (auto pair : primaryControllers) {
+        auto controllerPose = myAvatar->getControllerPoseInAvatarFrame(pair.first);
+        if (controllerPose.isValid()) {
+            AnimPose pose(controllerPose.getRotation(), controllerPose.getTranslation());
+            params.primaryControllerPoses[pair.second] = avatarToRigPose * pose;
+            params.primaryControllerActiveFlags[pair.second] = true;
+        } else {
+            params.primaryControllerPoses[pair.second] = AnimPose::identity;
+            params.primaryControllerActiveFlags[pair.second] = false;
+        }
     }
 
-    auto avatarSpine2Pose = myAvatar->getSpine2ControllerPoseInAvatarFrame();
-    if (avatarSpine2Pose.isValid()) {
-        AnimPose pose(avatarSpine2Pose.getRotation(), avatarSpine2Pose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_Spine2] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_Spine2] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_Spine2] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_Spine2] = false;
-    }
+    //
+    // secondary controller poses, influence the pose of the skeleton indirectly.
+    //
 
-    auto avatarRightArmPose = myAvatar->getRightArmControllerPoseInAvatarFrame();
-    if (avatarRightArmPose.isValid()) {
-        AnimPose pose(avatarRightArmPose.getRotation(), avatarRightArmPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_RightArm] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_RightArm] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_RightArm] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_RightArm] = false;
-    }
-    
-    auto avatarLeftArmPose = myAvatar->getLeftArmControllerPoseInAvatarFrame();
-    if (avatarLeftArmPose.isValid()) {
-        AnimPose pose(avatarLeftArmPose.getRotation(), avatarLeftArmPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_LeftArm] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_LeftArm] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_LeftArm] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_LeftArm] = false;
-    }
+    static const std::vector<std::pair<controller::Action, Rig::SecondaryControllerType>> secondaryControllers = {
+        { controller::Action::LEFT_SHOULDER, Rig::SecondaryControllerType_LeftShoulder },
+        { controller::Action::RIGHT_SHOULDER, Rig::SecondaryControllerType_RightShoulder },
+        { controller::Action::LEFT_ARM, Rig::SecondaryControllerType_LeftArm },
+        { controller::Action::RIGHT_ARM, Rig::SecondaryControllerType_RightArm },
+        { controller::Action::LEFT_FORE_ARM, Rig::SecondaryControllerType_LeftForeArm },
+        { controller::Action::RIGHT_FORE_ARM, Rig::SecondaryControllerType_RightForeArm },
+        { controller::Action::LEFT_UP_LEG, Rig::SecondaryControllerType_LeftUpLeg },
+        { controller::Action::RIGHT_UP_LEG, Rig::SecondaryControllerType_RightUpLeg },
+        { controller::Action::LEFT_LEG, Rig::SecondaryControllerType_LeftLeg },
+        { controller::Action::RIGHT_LEG, Rig::SecondaryControllerType_RightLeg },
+        { controller::Action::LEFT_TOE_BASE, Rig::SecondaryControllerType_LeftToeBase },
+        { controller::Action::RIGHT_TOE_BASE, Rig::SecondaryControllerType_RightToeBase }
+    };
 
-    auto avatarLeftHandPose = myAvatar->getLeftHandControllerPoseInAvatarFrame();
-    if (avatarLeftHandPose.isValid()) {
-        AnimPose pose(avatarLeftHandPose.getRotation(), avatarLeftHandPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_LeftHand] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_LeftHand] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_LeftHand] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_LeftHand] = false;
-    }
-
-    auto avatarRightHandPose = myAvatar->getRightHandControllerPoseInAvatarFrame();
-    if (avatarRightHandPose.isValid()) {
-        AnimPose pose(avatarRightHandPose.getRotation(), avatarRightHandPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_RightHand] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_RightHand] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_RightHand] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_RightHand] = false;
-    }
-
-    auto avatarLeftFootPose = myAvatar->getLeftFootControllerPoseInAvatarFrame();
-    if (avatarLeftFootPose.isValid()) {
-        AnimPose pose(avatarLeftFootPose.getRotation(), avatarLeftFootPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_LeftFoot] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_LeftFoot] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_LeftFoot] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_LeftFoot] = false;
-    }
-
-    auto avatarRightFootPose = myAvatar->getRightFootControllerPoseInAvatarFrame();
-    if (avatarRightFootPose.isValid()) {
-        AnimPose pose(avatarRightFootPose.getRotation(), avatarRightFootPose.getTranslation());
-        params.controllerPoses[Rig::ControllerType_RightFoot] = avatarToRigPose * pose;
-        params.controllerActiveFlags[Rig::ControllerType_RightFoot] = true;
-    } else {
-        params.controllerPoses[Rig::ControllerType_RightFoot] = AnimPose::identity;
-        params.controllerActiveFlags[Rig::ControllerType_RightFoot] = false;
+    for (auto pair : secondaryControllers) {
+        auto controllerPose = myAvatar->getControllerPoseInAvatarFrame(pair.first);
+        if (controllerPose.isValid()) {
+            AnimPose pose(controllerPose.getRotation(), controllerPose.getTranslation());
+            params.secondaryControllerPoses[pair.second] = avatarToRigPose * pose;
+            params.secondaryControllerActiveFlags[pair.second] = true;
+        } else {
+            params.secondaryControllerPoses[pair.second] = AnimPose::identity;
+            params.secondaryControllerActiveFlags[pair.second] = false;
+        }
     }
 
     params.bodyCapsuleRadius = myAvatar->getCharacterController()->getCapsuleRadius();
@@ -175,49 +152,106 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
 
     _rig.updateFromEyeParameters(eyeParams);
 
-    updateFingers(myAvatar->getLeftHandFingerControllerPosesInSensorFrame());
-    updateFingers(myAvatar->getRightHandFingerControllerPosesInSensorFrame());
+    updateFingers();
 }
 
 
-void MySkeletonModel::updateFingers(const MyAvatar::FingerPosesMap& fingerPoses) {
-    // Assumes that finger poses are kept in order in the poses map.
-
-    if (fingerPoses.size() == 0) {
-        return;
-}
-
-    auto posesMapItr = fingerPoses.begin();
-
-    bool isLeftHand = posesMapItr->first < (int)controller::Action::RIGHT_HAND_THUMB1;
+void MySkeletonModel::updateFingers() {
 
     MyAvatar* myAvatar = static_cast<MyAvatar*>(_owningAvatar);
-    auto handPose = isLeftHand 
-        ? myAvatar->getLeftHandControllerPoseInSensorFrame() 
-        : myAvatar->getRightHandControllerPoseInSensorFrame();
-    auto handJointRotation = handPose.getRotation();
 
-    bool isHandValid = handPose.isValid();
-    bool isFingerValid = false;
-    glm::quat previousJointRotation;
-
-    while (posesMapItr != fingerPoses.end()) {
-        auto jointName = posesMapItr->second.second;
-        if (isHandValid && jointName.right(1) == "1") {
-            isFingerValid = posesMapItr->second.first.isValid();
-            previousJointRotation = handJointRotation;
+    static std::vector<std::vector<std::pair<controller::Action, QString>>> fingerChains = {
+        {
+            { controller::Action::LEFT_HAND, "LeftHand" },
+            { controller::Action::LEFT_HAND_THUMB1, "LeftHandThumb1" },
+            { controller::Action::LEFT_HAND_THUMB2, "LeftHandThumb2" },
+            { controller::Action::LEFT_HAND_THUMB3, "LeftHandThumb3" },
+            { controller::Action::LEFT_HAND_THUMB4, "LeftHandThumb4" }
+        },
+        {
+            { controller::Action::LEFT_HAND, "LeftHand" },
+            { controller::Action::LEFT_HAND_INDEX1, "LeftHandIndex1" },
+            { controller::Action::LEFT_HAND_INDEX2, "LeftHandIndex2" },
+            { controller::Action::LEFT_HAND_INDEX3, "LeftHandIndex3" },
+            { controller::Action::LEFT_HAND_INDEX4, "LeftHandIndex4" }
+        },
+        {
+            { controller::Action::LEFT_HAND, "LeftHand" },
+            { controller::Action::LEFT_HAND_MIDDLE1, "LeftHandMiddle1" },
+            { controller::Action::LEFT_HAND_MIDDLE2, "LeftHandMiddle2" },
+            { controller::Action::LEFT_HAND_MIDDLE3, "LeftHandMiddle3" },
+            { controller::Action::LEFT_HAND_MIDDLE4, "LeftHandMiddle4" }
+        },
+        {
+            { controller::Action::LEFT_HAND, "LeftHand" },
+            { controller::Action::LEFT_HAND_RING1, "LeftHandRing1" },
+            { controller::Action::LEFT_HAND_RING2, "LeftHandRing2" },
+            { controller::Action::LEFT_HAND_RING3, "LeftHandRing3" },
+            { controller::Action::LEFT_HAND_RING4, "LeftHandRing4" }
+        },
+        {
+            { controller::Action::LEFT_HAND, "LeftHand" },
+            { controller::Action::LEFT_HAND_PINKY1, "LeftHandPinky1" },
+            { controller::Action::LEFT_HAND_PINKY2, "LeftHandPinky2" },
+            { controller::Action::LEFT_HAND_PINKY3, "LeftHandPinky3" },
+            { controller::Action::LEFT_HAND_PINKY4, "LeftHandPinky4" }
+        },
+        {
+            { controller::Action::RIGHT_HAND, "RightHand" },
+            { controller::Action::RIGHT_HAND_THUMB1, "RightHandThumb1" },
+            { controller::Action::RIGHT_HAND_THUMB2, "RightHandThumb2" },
+            { controller::Action::RIGHT_HAND_THUMB3, "RightHandThumb3" },
+            { controller::Action::RIGHT_HAND_THUMB4, "RightHandThumb4" }
+        },
+        {
+            { controller::Action::RIGHT_HAND, "RightHand" },
+            { controller::Action::RIGHT_HAND_INDEX1, "RightHandIndex1" },
+            { controller::Action::RIGHT_HAND_INDEX2, "RightHandIndex2" },
+            { controller::Action::RIGHT_HAND_INDEX3, "RightHandIndex3" },
+            { controller::Action::RIGHT_HAND_INDEX4, "RightHandIndex4" }
+        },
+        {
+            { controller::Action::RIGHT_HAND, "RightHand" },
+            { controller::Action::RIGHT_HAND_MIDDLE1, "RightHandMiddle1" },
+            { controller::Action::RIGHT_HAND_MIDDLE2, "RightHandMiddle2" },
+            { controller::Action::RIGHT_HAND_MIDDLE3, "RightHandMiddle3" },
+            { controller::Action::RIGHT_HAND_MIDDLE4, "RightHandMiddle4" }
+        },
+        {
+            { controller::Action::RIGHT_HAND, "RightHand" },
+            { controller::Action::RIGHT_HAND_RING1, "RightHandRing1" },
+            { controller::Action::RIGHT_HAND_RING2, "RightHandRing2" },
+            { controller::Action::RIGHT_HAND_RING3, "RightHandRing3" },
+            { controller::Action::RIGHT_HAND_RING4, "RightHandRing4" }
+        },
+        {
+            { controller::Action::RIGHT_HAND, "RightHand" },
+            { controller::Action::RIGHT_HAND_PINKY1, "RightHandPinky1" },
+            { controller::Action::RIGHT_HAND_PINKY2, "RightHandPinky2" },
+            { controller::Action::RIGHT_HAND_PINKY3, "RightHandPinky3" },
+            { controller::Action::RIGHT_HAND_PINKY4, "RightHandPinky4" }
         }
+    };
 
-        if (isHandValid && isFingerValid) {
-            auto thisJointRotation = posesMapItr->second.first.getRotation();
-            const float CONTROLLER_PRIORITY = 2.0f;
-            _rig.setJointRotation(_rig.indexOfJoint(jointName), true, glm::inverse(previousJointRotation) * thisJointRotation, 
-                CONTROLLER_PRIORITY);
-            previousJointRotation = thisJointRotation;
-        } else {
-            _rig.clearJointAnimationPriority(_rig.indexOfJoint(jointName));
+    const float CONTROLLER_PRIORITY = 2.0f;
+
+    for (auto& chain : fingerChains) {
+        glm::quat prevAbsRot = Quaternions::IDENTITY;
+        for (auto& link : chain) {
+            int index = _rig.indexOfJoint(link.second);
+            if (index >= 0) {
+                auto pose = myAvatar->getControllerPoseInSensorFrame(link.first);
+                if (pose.valid) {
+                    glm::quat relRot = glm::inverse(prevAbsRot) * pose.getRotation();
+                    // only set the rotation for the finger joints, not the hands.
+                    if (link.first != controller::Action::LEFT_HAND && link.first != controller::Action::RIGHT_HAND) {
+                        _rig.setJointRotation(index, true, relRot, CONTROLLER_PRIORITY);
+                    }
+                    prevAbsRot = pose.getRotation();
+                } else {
+                    _rig.clearJointAnimationPriority(index);
+                }
+            }
         }
-
-        posesMapItr++;
     }
 }
diff --git a/interface/src/avatar/MySkeletonModel.h b/interface/src/avatar/MySkeletonModel.h
index 6867c596af..ad0ae1b8e9 100644
--- a/interface/src/avatar/MySkeletonModel.h
+++ b/interface/src/avatar/MySkeletonModel.h
@@ -24,7 +24,7 @@ public:
     void updateRig(float deltaTime, glm::mat4 parentTransform) override;
 
 private:
-    void updateFingers(const MyAvatar::FingerPosesMap& fingerPoses);
+    void updateFingers();
 };
 
 #endif // hifi_MySkeletonModel_h
diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp
index d02f4d8fcf..f2e6dbf4d7 100644
--- a/interface/src/scripting/AudioDevices.cpp
+++ b/interface/src/scripting/AudioDevices.cpp
@@ -12,6 +12,7 @@
 #include <map>
 
 #include <shared/QtHelpers.h>
+#include <plugins/DisplayPlugin.h>
 
 #include "AudioDevices.h"
 
diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp
index 7b9592fa4c..5b7c1896fe 100644
--- a/interface/src/ui/ResourceImageItem.cpp
+++ b/interface/src/ui/ResourceImageItem.cpp
@@ -8,7 +8,6 @@
 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 
-//#include "Application.h"
 #include "ResourceImageItem.h"
 
 #include <QOpenGLFramebufferObjectFormat>
@@ -16,6 +15,8 @@
 #include <QOpenGLExtraFunctions>
 #include <QOpenGLContext>
 
+#include <plugins/DisplayPlugin.h>
+
 ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() {
     auto textureCache = DependencyManager::get<TextureCache>();
     connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update()));
diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp
index 70767b007d..3c00be8358 100644
--- a/interface/src/ui/SnapshotAnimated.cpp
+++ b/interface/src/ui/SnapshotAnimated.cpp
@@ -13,8 +13,9 @@
 #include <QtCore/QObject>
 #include <QtCore/QString>
 #include <QtGui/QImage>
+#include <QtConcurrent/QtConcurrentRun>
 
-#include <QtConcurrent/qtconcurrentrun.h>
+#include <plugins/DisplayPlugin.h>
 #include "SnapshotAnimated.h"
 
 QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL;
diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp
index e406d139d0..77b9424d2f 100644
--- a/interface/src/ui/overlays/ContextOverlayInterface.cpp
+++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp
@@ -257,15 +257,23 @@ void ContextOverlayInterface::openMarketplace() {
 }
 
 void ContextOverlayInterface::enableEntityHighlight(const EntityItemID& entityItemID) {
-    if (!qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->getShouldHighlight()) {
-        qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'true' for Entity ID:" << entityItemID;
-        qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->setShouldHighlight(true);
-    }
+    auto entityTree = qApp->getEntities()->getTree();
+    entityTree->withReadLock([&] {
+        auto entityItem = entityTree->findEntityByEntityItemID(entityItemID);
+        if ((entityItem != NULL) && !entityItem->getShouldHighlight()) {
+            qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'true' for Entity ID:" << entityItemID;
+            entityItem->setShouldHighlight(true);
+        }
+    });
 }
 
 void ContextOverlayInterface::disableEntityHighlight(const EntityItemID& entityItemID) {
-    if (qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->getShouldHighlight()) {
-        qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'false' for Entity ID:" << entityItemID;
-        qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->setShouldHighlight(false);
-    }
+    auto entityTree = qApp->getEntities()->getTree();
+    entityTree->withReadLock([&] {
+        auto entityItem = entityTree->findEntityByEntityItemID(entityItemID);
+        if ((entityItem != NULL) && entityItem->getShouldHighlight()) {
+            qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'false' for Entity ID:" << entityItemID;
+            entityItem->setShouldHighlight(false);
+        }
+    });
 }
diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp
index d61370151c..8e58b648e6 100644
--- a/interface/src/ui/overlays/Web3DOverlay.cpp
+++ b/interface/src/ui/overlays/Web3DOverlay.cpp
@@ -127,8 +127,9 @@ QString Web3DOverlay::pickURL() {
     QUrl sourceUrl(_url);
     if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" ||
         _url.toLower().endsWith(".htm") || _url.toLower().endsWith(".html")) {
-
-        _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/"));
+        if (_webSurface) {
+            _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/"));
+        }
         return "Web3DOverlay.qml";
     } else {
         return QUrl::fromLocalFile(PathUtils::resourcesPath()).toString() + "/" + _url;
@@ -252,7 +253,7 @@ void Web3DOverlay::render(RenderArgs* args) {
             if (overlayID == selfOverlayID && (self->_pressed || (!self->_activeTouchPoints.empty() && self->_touchBeginAccepted))) {
                 PointerEvent endEvent(PointerEvent::Release, event.getID(), event.getPos2D(), event.getPos3D(), event.getNormal(), event.getDirection(),
                                       event.getButton(), event.getButtons(), event.getKeyboardModifiers());
-                forwardPointerEvent(overlayID, event);
+                forwardPointerEvent(overlayID, endEvent);
             }
         });
 
diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp
index 57c00e7183..d17fbebf3a 100644
--- a/libraries/animation/src/AnimInverseKinematics.cpp
+++ b/libraries/animation/src/AnimInverseKinematics.cpp
@@ -23,12 +23,13 @@
 #include "CubicHermiteSpline.h"
 #include "AnimUtil.h"
 
+static const int MAX_TARGET_MARKERS = 30;
 static const float JOINT_CHAIN_INTERP_TIME = 0.25f;
 
 static void lookupJointInfo(const AnimInverseKinematics::JointChainInfo& jointChainInfo,
-                                 int indexA, int indexB,
-                                 const AnimInverseKinematics::JointInfo** jointInfoA,
-                                 const AnimInverseKinematics::JointInfo** jointInfoB) {
+                            int indexA, int indexB,
+                            const AnimInverseKinematics::JointInfo** jointInfoA,
+                            const AnimInverseKinematics::JointInfo** jointInfoB) {
     *jointInfoA = nullptr;
     *jointInfoB = nullptr;
     for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) {
@@ -97,6 +98,12 @@ AnimInverseKinematics::~AnimInverseKinematics() {
     _rotationAccumulators.clear();
     _translationAccumulators.clear();
     _targetVarVec.clear();
+
+    // remove markers
+    for (int i = 0; i < MAX_TARGET_MARKERS; i++) {
+        QString name = QString("ikTarget%1").arg(i);
+        DebugDraw::getInstance().removeMyAvatarMarker(name);
+    }
 }
 
 void AnimInverseKinematics::loadDefaultPoses(const AnimPoseVec& poses) {
@@ -1015,19 +1022,30 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
                 // debug render ik targets
                 if (context.getEnableDebugDrawIKTargets()) {
                     const vec4 WHITE(1.0f);
+                    const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f);
                     glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3());
+                    int targetNum = 0;
 
                     for (auto& target : targets) {
                         glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation());
                         glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat;
 
-                        QString name = QString("ikTarget%1").arg(target.getIndex());
+                        QString name = QString("ikTarget%1").arg(targetNum);
                         DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE);
+                        targetNum++;
+                    }
+
+                    // draw secondary ik targets
+                    for (auto& iter : _secondaryTargetsInRigFrame) {
+                        glm::mat4 avatarTargetMat = rigToAvatarMat * (glm::mat4)iter.second;
+                        QString name = QString("ikTarget%1").arg(targetNum);
+                        DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), GREEN);
+                        targetNum++;
                     }
                 } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) {
                     // remove markers if they were added last frame.
-                    for (auto& target : targets) {
-                        QString name = QString("ikTarget%1").arg(target.getIndex());
+                    for (int i = 0; i < MAX_TARGET_MARKERS; i++) {
+                        QString name = QString("ikTarget%1").arg(i);
                         DebugDraw::getInstance().removeMyAvatarMarker(name);
                     }
                 }
@@ -1038,7 +1056,9 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
             {
                 PROFILE_RANGE_EX(simulation_animation, "ik/ccd", 0xffff00ff, 0);
 
+                setSecondaryTargets(context);
                 preconditionRelativePosesToAvoidLimbLock(context, targets);
+
                 solve(context, targets, dt, jointChainInfoVec);
             }
 
@@ -1125,6 +1145,22 @@ void AnimInverseKinematics::clearIKJointLimitHistory() {
     }
 }
 
+void AnimInverseKinematics::setSecondaryTargetInRigFrame(int jointIndex, const AnimPose& pose) {
+    auto iter = _secondaryTargetsInRigFrame.find(jointIndex);
+    if (iter != _secondaryTargetsInRigFrame.end()) {
+        iter->second = pose;
+    } else {
+        _secondaryTargetsInRigFrame.insert({ jointIndex, pose });
+    }
+}
+
+void AnimInverseKinematics::clearSecondaryTarget(int jointIndex) {
+    auto iter = _secondaryTargetsInRigFrame.find(jointIndex);
+    if (iter != _secondaryTargetsInRigFrame.end()) {
+        _secondaryTargetsInRigFrame.erase(iter);
+    }
+}
+
 RotationConstraint* AnimInverseKinematics::getConstraint(int index) const {
     RotationConstraint* constraint = nullptr;
     std::map<int, RotationConstraint*>::const_iterator constraintItr = _constraints.find(index);
@@ -1575,7 +1611,7 @@ void AnimInverseKinematics::debugDrawRelativePoses(const AnimContext& context) c
     const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f);
     const vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f);
     const vec4 GRAY(0.2f, 0.2f, 0.2f, 1.0f);
-    const float AXIS_LENGTH = 2.0f; // cm
+    const float AXIS_LENGTH = 10.0f; // cm
 
     // draw each pose
     for (int i = 0; i < (int)poses.size(); i++) {
@@ -1605,8 +1641,10 @@ void AnimInverseKinematics::debugDrawIKChain(const JointChainInfo& jointChainInf
     // copy debug joint rotations into the relative poses
     for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) {
         const JointInfo& info = jointChainInfo.jointInfoVec[i];
-        poses[info.jointIndex].rot() = info.rot;
-        poses[info.jointIndex].trans() = info.trans;
+        if (info.jointIndex != _hipsIndex) {
+            poses[info.jointIndex].rot() = info.rot;
+            poses[info.jointIndex].trans() = info.trans;
+        }
     }
 
     // convert relative poses to absolute
@@ -1825,6 +1863,59 @@ void AnimInverseKinematics::preconditionRelativePosesToAvoidLimbLock(const AnimC
     }
 }
 
+// overwrites _relativePoses with secondary poses.
+void AnimInverseKinematics::setSecondaryTargets(const AnimContext& context) {
+
+    if (_secondaryTargetsInRigFrame.empty()) {
+        return;
+    }
+
+    // special case for arm secondary poses.
+    // determine if shoulder joint should look-at position of arm joint.
+    bool shoulderShouldLookAtArm = false;
+    const int leftArmIndex = _skeleton->nameToJointIndex("LeftArm");
+    const int rightArmIndex = _skeleton->nameToJointIndex("RightArm");
+    const int leftShoulderIndex = _skeleton->nameToJointIndex("LeftShoulder");
+    const int rightShoulderIndex = _skeleton->nameToJointIndex("RightShoulder");
+    for (auto& iter : _secondaryTargetsInRigFrame) {
+        if (iter.first == leftShoulderIndex || iter.first == rightShoulderIndex) {
+            shoulderShouldLookAtArm = true;
+            break;
+        }
+    }
+
+    AnimPose rigToGeometryPose = AnimPose(glm::inverse(context.getGeometryToRigMatrix()));
+    for (auto& iter : _secondaryTargetsInRigFrame) {
+        AnimPose absPose = rigToGeometryPose * iter.second;
+        absPose.scale() = glm::vec3(1.0f);
+
+        AnimPose parentAbsPose;
+        int parentIndex = _skeleton->getParentIndex(iter.first);
+        if (parentIndex >= 0) {
+            parentAbsPose = _skeleton->getAbsolutePose(parentIndex, _relativePoses);
+        }
+
+        // if parent should "look-at" child joint position.
+        if (shoulderShouldLookAtArm && (iter.first == leftArmIndex || iter.first == rightArmIndex)) {
+
+            AnimPose grandParentAbsPose;
+            int grandParentIndex = _skeleton->getParentIndex(parentIndex);
+            if (parentIndex >= 0) {
+                grandParentAbsPose = _skeleton->getAbsolutePose(grandParentIndex, _relativePoses);
+            }
+
+            // the shoulder should rotate toward the arm joint via "look-at" constraint
+            parentAbsPose = boneLookAt(absPose.trans(), parentAbsPose);
+            _relativePoses[parentIndex] = grandParentAbsPose.inverse() * parentAbsPose;
+        }
+
+        // Ignore translation on secondary poses, to prevent them from distorting the skeleton.
+        glm::vec3 origTrans = _relativePoses[iter.first].trans();
+        _relativePoses[iter.first] = parentAbsPose.inverse() * absPose;
+        _relativePoses[iter.first].trans() = origTrans;
+    }
+}
+
 void AnimInverseKinematics::initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPoses) {
     const float RELAX_BLEND_FACTOR = (1.0f / 16.0f);
     const float COPY_BLEND_FACTOR = 1.0f;
diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h
index 7f7640aa24..bdfbad408d 100644
--- a/libraries/animation/src/AnimInverseKinematics.h
+++ b/libraries/animation/src/AnimInverseKinematics.h
@@ -70,6 +70,9 @@ public:
         NumSolutionSources,
     };
 
+    void setSecondaryTargetInRigFrame(int jointIndex, const AnimPose& pose);
+    void clearSecondaryTarget(int jointIndex);
+
     void setSolutionSource(SolutionSource solutionSource) { _solutionSource = solutionSource; }
     void setSolutionSourceVar(const QString& solutionSourceVar) { _solutionSourceVar = solutionSourceVar; }
 
@@ -88,6 +91,7 @@ protected:
     void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose);
     void blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPose, float blendFactor);
     void preconditionRelativePosesToAvoidLimbLock(const AnimContext& context, const std::vector<IKTarget>& targets);
+    void setSecondaryTargets(const AnimContext& context);
     AnimPose applyHipsOffset() const;
 
     // used to pre-compute information about each joint influeced by a spline IK target.
@@ -142,6 +146,8 @@ protected:
     AnimPoseVec _relativePoses; // current relative poses
     AnimPoseVec _limitCenterPoses;  // relative
 
+    std::map<int, AnimPose> _secondaryTargetsInRigFrame;
+
     mutable std::map<int, std::vector<SplineJointInfo>> _splineJointInfoMap;
 
     // experimental data for moving hips during IK
diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp
index bcf30642e8..65c605b5ba 100644
--- a/libraries/animation/src/AnimUtil.cpp
+++ b/libraries/animation/src/AnimUtil.cpp
@@ -96,3 +96,14 @@ float accumulateTime(float startFrame, float endFrame, float timeScale, float cu
     return frame;
 }
 
+// rotate bone's y-axis with target.
+AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone) {
+    glm::vec3 u, v, w;
+    generateBasisVectors(target - bone.trans(), bone.rot() * Vectors::UNIT_X, u, v, w);
+    glm::mat4 lookAt(glm::vec4(v, 0.0f),
+                     glm::vec4(u, 0.0f),
+                     // AJT: TODO REVISIT THIS, this could be -w.
+                     glm::vec4(glm::normalize(glm::cross(v, u)), 0.0f),
+                     glm::vec4(bone.trans(), 1.0f));
+    return AnimPose(lookAt);
+}
diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h
index d215fdc654..f2cceb361b 100644
--- a/libraries/animation/src/AnimUtil.h
+++ b/libraries/animation/src/AnimUtil.h
@@ -31,4 +31,6 @@ inline glm::quat safeLerp(const glm::quat& a, const glm::quat& b, float alpha) {
     return glm::normalize(glm::lerp(a, bTemp, alpha));
 }
 
+AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone);
+
 #endif
diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp
index fc0ca73c96..21f98d3e01 100644
--- a/libraries/animation/src/Rig.cpp
+++ b/libraries/animation/src/Rig.cpp
@@ -1399,24 +1399,25 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
     _animVars.set("isTalking", params.isTalking);
     _animVars.set("notIsTalking", !params.isTalking);
 
-    bool headEnabled = params.controllerActiveFlags[ControllerType_Head];
-    bool leftHandEnabled = params.controllerActiveFlags[ControllerType_LeftHand];
-    bool rightHandEnabled = params.controllerActiveFlags[ControllerType_RightHand];
-    bool hipsEnabled = params.controllerActiveFlags[ControllerType_Hips];
-    bool leftFootEnabled = params.controllerActiveFlags[ControllerType_LeftFoot];
-    bool rightFootEnabled = params.controllerActiveFlags[ControllerType_RightFoot];
-    bool leftArmEnabled = params.controllerActiveFlags[ControllerType_LeftArm];
-    bool rightArmEnabled = params.controllerActiveFlags[ControllerType_RightArm];
-    bool spine2Enabled = params.controllerActiveFlags[ControllerType_Spine2];
+    bool headEnabled = params.primaryControllerActiveFlags[PrimaryControllerType_Head];
+    bool leftHandEnabled = params.primaryControllerActiveFlags[PrimaryControllerType_LeftHand];
+    bool rightHandEnabled = params.primaryControllerActiveFlags[PrimaryControllerType_RightHand];
+    bool hipsEnabled = params.primaryControllerActiveFlags[PrimaryControllerType_Hips];
+    bool leftFootEnabled = params.primaryControllerActiveFlags[PrimaryControllerType_LeftFoot];
+    bool rightFootEnabled = params.primaryControllerActiveFlags[PrimaryControllerType_RightFoot];
+    bool spine2Enabled = params.primaryControllerActiveFlags[PrimaryControllerType_Spine2];
 
-    updateHead(headEnabled, hipsEnabled, params.controllerPoses[ControllerType_Head]);
+    bool leftArmEnabled = params.secondaryControllerActiveFlags[SecondaryControllerType_LeftArm];
+    bool rightArmEnabled = params.secondaryControllerActiveFlags[SecondaryControllerType_RightArm];
+
+    updateHead(headEnabled, hipsEnabled, params.primaryControllerPoses[PrimaryControllerType_Head]);
 
     updateHands(leftHandEnabled, rightHandEnabled, hipsEnabled, leftArmEnabled, rightArmEnabled, dt,
-                params.controllerPoses[ControllerType_LeftHand], params.controllerPoses[ControllerType_RightHand],
+                params.primaryControllerPoses[PrimaryControllerType_LeftHand], params.primaryControllerPoses[PrimaryControllerType_RightHand],
                 params.bodyCapsuleRadius, params.bodyCapsuleHalfHeight, params.bodyCapsuleLocalOffset);
 
     updateFeet(leftFootEnabled, rightFootEnabled,
-               params.controllerPoses[ControllerType_LeftFoot], params.controllerPoses[ControllerType_RightFoot]);
+               params.primaryControllerPoses[PrimaryControllerType_LeftFoot], params.primaryControllerPoses[PrimaryControllerType_RightFoot]);
 
     // if the hips or the feet are being controlled.
     if (hipsEnabled || rightFootEnabled || leftFootEnabled) {
@@ -1437,34 +1438,46 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo
 
     if (hipsEnabled) {
         _animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition);
-        _animVars.set("hipsPosition", params.controllerPoses[ControllerType_Hips].trans());
-        _animVars.set("hipsRotation", params.controllerPoses[ControllerType_Hips].rot());
+        _animVars.set("hipsPosition", params.primaryControllerPoses[PrimaryControllerType_Hips].trans());
+        _animVars.set("hipsRotation", params.primaryControllerPoses[PrimaryControllerType_Hips].rot());
     } else {
         _animVars.set("hipsType", (int)IKTarget::Type::Unknown);
     }
 
     if (hipsEnabled && spine2Enabled) {
         _animVars.set("spine2Type", (int)IKTarget::Type::Spline);
-        _animVars.set("spine2Position", params.controllerPoses[ControllerType_Spine2].trans());
-        _animVars.set("spine2Rotation", params.controllerPoses[ControllerType_Spine2].rot());
+        _animVars.set("spine2Position", params.primaryControllerPoses[PrimaryControllerType_Spine2].trans());
+        _animVars.set("spine2Rotation", params.primaryControllerPoses[PrimaryControllerType_Spine2].rot());
     } else {
         _animVars.set("spine2Type", (int)IKTarget::Type::Unknown);
     }
 
-    if (leftArmEnabled) {
-        _animVars.set("leftArmType", (int)IKTarget::Type::RotationAndPosition);
-        _animVars.set("leftArmPosition", params.controllerPoses[ControllerType_LeftArm].trans());
-        _animVars.set("leftArmRotation", params.controllerPoses[ControllerType_LeftArm].rot());
-    } else {
-        _animVars.set("leftArmType", (int)IKTarget::Type::Unknown);
-    }
+    // set secondary targets
+    static const std::vector<QString> secondaryControllerJointNames = {
+        "LeftShoulder",
+        "RightShoulder",
+        "LeftArm",
+        "RightArm",
+        "LeftForeArm",
+        "RightForeArm",
+        "LeftUpLeg",
+        "RightUpLeg",
+        "LeftLeg",
+        "RightLeg",
+        "LeftToeBase",
+        "RightToeBase"
+    };
 
-    if (rightArmEnabled) {
-        _animVars.set("rightArmType", (int)IKTarget::Type::RotationAndPosition);
-        _animVars.set("rightArmPosition", params.controllerPoses[ControllerType_RightArm].trans());
-        _animVars.set("rightArmRotation", params.controllerPoses[ControllerType_RightArm].rot());
-    } else {
-        _animVars.set("rightArmType", (int)IKTarget::Type::Unknown);
+    std::shared_ptr<AnimInverseKinematics> ikNode = getAnimInverseKinematicsNode();
+    for (int i = 0; i < (int)NumSecondaryControllerTypes; i++) {
+        int index = indexOfJoint(secondaryControllerJointNames[i]);
+        if (index >= 0) {
+            if (params.secondaryControllerActiveFlags[i]) {
+                ikNode->setSecondaryTargetInRigFrame(index, params.secondaryControllerPoses[i]);
+            } else {
+                ikNode->clearSecondaryTarget(index);
+            }
+        }
     }
 }
 
diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h
index 5293fa1fe7..7e1504e461 100644
--- a/libraries/animation/src/Rig.h
+++ b/libraries/animation/src/Rig.h
@@ -41,22 +41,39 @@ public:
         bool useNames;
     };
 
-    enum ControllerType {
-        ControllerType_Head = 0,
-        ControllerType_LeftHand,
-        ControllerType_RightHand,
-        ControllerType_Hips,
-        ControllerType_LeftFoot,
-        ControllerType_RightFoot,
-        ControllerType_LeftArm,
-        ControllerType_RightArm,
-        ControllerType_Spine2,
-        NumControllerTypes
+    enum PrimaryControllerType {
+        PrimaryControllerType_Head = 0,
+        PrimaryControllerType_LeftHand,
+        PrimaryControllerType_RightHand,
+        PrimaryControllerType_Hips,
+        PrimaryControllerType_LeftFoot,
+        PrimaryControllerType_RightFoot,
+        PrimaryControllerType_Spine2,
+        NumPrimaryControllerTypes
+    };
+
+    // NOTE: These should ordered such that joint parents appear before their children.
+    enum SecondaryControllerType {
+        SecondaryControllerType_LeftShoulder = 0,
+        SecondaryControllerType_RightShoulder,
+        SecondaryControllerType_LeftArm,
+        SecondaryControllerType_RightArm,
+        SecondaryControllerType_LeftForeArm,
+        SecondaryControllerType_RightForeArm,
+        SecondaryControllerType_LeftUpLeg,
+        SecondaryControllerType_RightUpLeg,
+        SecondaryControllerType_LeftLeg,
+        SecondaryControllerType_RightLeg,
+        SecondaryControllerType_LeftToeBase,
+        SecondaryControllerType_RightToeBase,
+        NumSecondaryControllerTypes
     };
 
     struct ControllerParameters {
-        AnimPose controllerPoses[NumControllerTypes];  // rig space
-        bool controllerActiveFlags[NumControllerTypes];
+        AnimPose primaryControllerPoses[NumPrimaryControllerTypes];  // rig space
+        bool primaryControllerActiveFlags[NumPrimaryControllerTypes];
+        AnimPose secondaryControllerPoses[NumSecondaryControllerTypes];  // rig space
+        bool secondaryControllerActiveFlags[NumSecondaryControllerTypes];
         bool isTalking;
         float bodyCapsuleRadius;
         float bodyCapsuleHalfHeight;
diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h
index 6319b5746e..a133d62c9f 100644
--- a/libraries/controllers/src/controllers/Actions.h
+++ b/libraries/controllers/src/controllers/Actions.h
@@ -101,6 +101,7 @@ enum class Action {
     // Bisected aliases for TRANSLATE_CAMERA_Z
     BOOM_IN,
     BOOM_OUT,
+
     LEFT_ARM,
     RIGHT_ARM,
 
@@ -146,6 +147,17 @@ enum class Action {
     RIGHT_HAND_PINKY3,
     RIGHT_HAND_PINKY4,
 
+    LEFT_SHOULDER,
+    RIGHT_SHOULDER,
+    LEFT_FORE_ARM,
+    RIGHT_FORE_ARM,
+    LEFT_LEG,
+    RIGHT_LEG,
+    LEFT_UP_LEG,
+    RIGHT_UP_LEG,
+    LEFT_TOE_BASE,
+    RIGHT_TOE_BASE,
+
     TRACKED_OBJECT_00,
     TRACKED_OBJECT_01,
     TRACKED_OBJECT_02,
@@ -163,7 +175,7 @@ enum class Action {
     TRACKED_OBJECT_14,
     TRACKED_OBJECT_15,
 
-    NUM_ACTIONS,
+    NUM_ACTIONS
 };
 
 template <typename T>
diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
index 9a898b4071..ba8e0c18e7 100644
--- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
@@ -69,7 +69,7 @@ RenderableWebEntityItem::~RenderableWebEntityItem() {
     }
 }
 
-bool RenderableWebEntityItem::buildWebSurface(QSharedPointer<EntityTreeRenderer> renderer) {
+bool RenderableWebEntityItem::buildWebSurface() {
     if (_currentWebCount >= MAX_CONCURRENT_WEB_VIEWS) {
         qWarning() << "Too many concurrent web views to create new view";
         return false;
@@ -132,6 +132,8 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer<EntityTreeRenderer>
             handlePointerEvent(event);
         }
     };
+
+    auto renderer = DependencyManager::get<EntityTreeRenderer>();
     _mousePressConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mousePressOnEntity, forwardPointerEvent);
     _mouseReleaseConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseReleaseOnEntity, forwardPointerEvent);
     _mouseMoveConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseMoveOnEntity, forwardPointerEvent);
@@ -185,8 +187,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
     #endif
 
     if (!_webSurface) {
-        auto renderer = qSharedPointerCast<EntityTreeRenderer>(args->_renderData);
-        if (!buildWebSurface(renderer)) {
+        if (!buildWebSurface()) {
             return;
         }
         _fadeStartTime = usecTimestampNow();
diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h
index 0f5d307766..a2318081b6 100644
--- a/libraries/entities-renderer/src/RenderableWebEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h
@@ -57,7 +57,7 @@ public:
     virtual QObject* getRootItem() override;
 
 private:
-    bool buildWebSurface(QSharedPointer<EntityTreeRenderer> renderer);
+    bool buildWebSurface();
     void destroyWebSurface();
     glm::vec2 getWindowSize() const;
 
diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp
index 11e67811b6..eac74fbdf9 100644
--- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp
+++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp
@@ -196,8 +196,6 @@ GLBackend::GLBackend() {
 
 
 GLBackend::~GLBackend() {
-    resetStages();
-
     killInput();
     killTransform();
 }
diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h
index 88aecda617..1908db614d 100644
--- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h
+++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h
@@ -66,7 +66,7 @@ protected:
 public:
     static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings = Shader::BindingSet());
 
-    ~GLBackend();
+    virtual ~GLBackend();
 
     void setCameraCorrection(const Mat4& correction);
     void render(const Batch& batch) final override;
diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h
index 2c64b9d23d..42926fdb1c 100644
--- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h
+++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h
@@ -43,6 +43,11 @@ public:
 
     explicit GL41Backend(bool syncCache) : Parent(syncCache) {}
     GL41Backend() : Parent() {}
+    virtual ~GL41Backend() {
+        // call resetStages here rather than in ~GLBackend dtor because it will call releaseResourceBuffer
+        // which is pure virtual from GLBackend's dtor.
+        resetStages();
+    }
 
     static const std::string GL41_VERSION;
     const std::string& getVersion() const override { return GL41_VERSION; }
diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h
index d5ff1a3485..1a4b63d35f 100644
--- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h
+++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h
@@ -39,6 +39,11 @@ public:
 
     explicit GL45Backend(bool syncCache) : Parent(syncCache) {}
     GL45Backend() : Parent() {}
+    virtual ~GL45Backend() {
+        // call resetStages here rather than in ~GLBackend dtor because it will call releaseResourceBuffer
+        // which is pure virtual from GLBackend's dtor.
+        resetStages();
+    }
 
     static const std::string GL45_VERSION;
     const std::string& getVersion() const override { return GL45_VERSION; }
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index eab0e5e41f..e03ec5e771 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -220,7 +220,7 @@ bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) {
         const HifiSockAddr& senderSockAddr = packet.getSenderSockAddr();
         QUuid sourceID;
 
-        if (NON_SOURCED_PACKETS.contains(headerType)) {
+        if (PacketTypeEnum::getNonSourcedPackets().contains(headerType)) {
             hasBeenOutput = versionDebugSuppressMap.contains(senderSockAddr, headerType);
 
             if (!hasBeenOutput) {
@@ -256,8 +256,8 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
 
     PacketType headerType = NLPacket::typeInHeader(packet);
 
-    if (NON_SOURCED_PACKETS.contains(headerType)) {
-        if (REPLICATED_PACKET_MAPPING.key(headerType) != PacketType::Unknown) {
+    if (PacketTypeEnum::getNonSourcedPackets().contains(headerType)) {
+        if (PacketTypeEnum::getReplicatedPacketMapping().key(headerType) != PacketType::Unknown) {
             // this is a replicated packet type - make sure the socket that sent it to us matches
             // one from one of our current upstream nodes
 
@@ -298,7 +298,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
         SharedNodePointer matchingNode = nodeWithUUID(sourceID);
 
         if (matchingNode) {
-            if (!NON_VERIFIED_PACKETS.contains(headerType)) {
+            if (!PacketTypeEnum::getNonVerifiedPackets().contains(headerType)) {
 
                 QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet);
                 QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, matchingNode->getConnectionSecret());
@@ -345,13 +345,13 @@ void LimitedNodeList::collectPacketStats(const NLPacket& packet) {
 }
 
 void LimitedNodeList::fillPacketHeader(const NLPacket& packet, const QUuid& connectionSecret) {
-    if (!NON_SOURCED_PACKETS.contains(packet.getType())) {
+    if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) {
         packet.writeSourceID(getSessionUUID());
     }
 
     if (!connectionSecret.isNull()
-        && !NON_SOURCED_PACKETS.contains(packet.getType())
-        && !NON_VERIFIED_PACKETS.contains(packet.getType())) {
+        && !PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())
+        && !PacketTypeEnum::getNonVerifiedPackets().contains(packet.getType())) {
         packet.writeVerificationHashGivenSecret(connectionSecret);
     }
 }
diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp
index a11dd69753..5c5077691b 100644
--- a/libraries/networking/src/NLPacket.cpp
+++ b/libraries/networking/src/NLPacket.cpp
@@ -12,8 +12,8 @@
 #include "NLPacket.h"
 
 int NLPacket::localHeaderSize(PacketType type) {
-    bool nonSourced = NON_SOURCED_PACKETS.contains(type);
-    bool nonVerified = NON_VERIFIED_PACKETS.contains(type);
+    bool nonSourced = PacketTypeEnum::getNonSourcedPackets().contains(type);
+    bool nonVerified = PacketTypeEnum::getNonVerifiedPackets().contains(type);
     qint64 optionalSize = (nonSourced ? 0 : NUM_BYTES_RFC4122_UUID) + ((nonSourced || nonVerified) ? 0 : NUM_BYTES_MD5_HASH);
     return sizeof(PacketType) + sizeof(PacketVersion) + optionalSize;
 }
@@ -198,13 +198,13 @@ void NLPacket::readVersion() {
 }
 
 void NLPacket::readSourceID() {
-    if (!NON_SOURCED_PACKETS.contains(_type)) {
+    if (!PacketTypeEnum::getNonSourcedPackets().contains(_type)) {
         _sourceID = sourceIDInHeader(*this);
     }
 }
 
 void NLPacket::writeSourceID(const QUuid& sourceID) const {
-    Q_ASSERT(!NON_SOURCED_PACKETS.contains(_type));
+    Q_ASSERT(!PacketTypeEnum::getNonSourcedPackets().contains(_type));
     
     auto offset = Packet::totalHeaderSize(isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion);
     memcpy(_packet.get() + offset, sourceID.toRfc4122().constData(), NUM_BYTES_RFC4122_UUID);
@@ -213,7 +213,8 @@ void NLPacket::writeSourceID(const QUuid& sourceID) const {
 }
 
 void NLPacket::writeVerificationHashGivenSecret(const QUuid& connectionSecret) const {
-    Q_ASSERT(!NON_SOURCED_PACKETS.contains(_type) && !NON_VERIFIED_PACKETS.contains(_type));
+    Q_ASSERT(!PacketTypeEnum::getNonSourcedPackets().contains(_type) &&
+             !PacketTypeEnum::getNonVerifiedPackets().contains(_type));
     
     auto offset = Packet::totalHeaderSize(isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion)
                 + NUM_BYTES_RFC4122_UUID;
diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp
index d7f8f404e6..25eef38dbd 100644
--- a/libraries/networking/src/Node.cpp
+++ b/libraries/networking/src/Node.cpp
@@ -29,11 +29,9 @@ int NodePtrMetaTypeId = qRegisterMetaType<Node*>("Node*");
 int sharedPtrNodeMetaTypeId = qRegisterMetaType<QSharedPointer<Node>>("QSharedPointer<Node>");
 int sharedNodePtrMetaTypeId = qRegisterMetaType<SharedNodePointer>("SharedNodePointer");
 
-namespace NodeType {
-    QHash<NodeType_t, QString> TypeNameHash;
-}
-
 void NodeType::init() {
+    QHash<NodeType_t, QString>& TypeNameHash = Node::getTypeNameHash();
+
     TypeNameHash.insert(NodeType::DomainServer, "Domain Server");
     TypeNameHash.insert(NodeType::EntityServer, "Entity Server");
     TypeNameHash.insert(NodeType::Agent, "Agent");
@@ -50,6 +48,7 @@ void NodeType::init() {
 }
 
 const QString& NodeType::getNodeTypeName(NodeType_t nodeType) {
+    QHash<NodeType_t, QString>& TypeNameHash = Node::getTypeNameHash();
     QHash<NodeType_t, QString>::iterator matchedTypeName = TypeNameHash.find(nodeType);
     return matchedTypeName != TypeNameHash.end() ? matchedTypeName.value() : UNKNOWN_NodeType_t_NAME;
 }
@@ -85,6 +84,7 @@ NodeType_t NodeType::downstreamType(NodeType_t primaryType) {
 }
 
 NodeType_t NodeType::fromString(QString type) {
+    QHash<NodeType_t, QString>& TypeNameHash = Node::getTypeNameHash();
     return TypeNameHash.key(type, NodeType::Unassigned);
 }
 
diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h
index c20ff5a395..4d09f077bd 100644
--- a/libraries/networking/src/Node.h
+++ b/libraries/networking/src/Node.h
@@ -86,6 +86,11 @@ public:
 
     bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; }
 
+    static QHash<NodeType_t, QString>& getTypeNameHash() {
+        static QHash<NodeType_t, QString> TypeNameHash;
+        return TypeNameHash;
+    }
+
 private:
     // privatize copy and assignment operator to disallow Node copying
     Node(const Node &otherNode);
diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp
index 21db207375..556e55beb2 100644
--- a/libraries/networking/src/PacketReceiver.cpp
+++ b/libraries/networking/src/PacketReceiver.cpp
@@ -32,7 +32,7 @@ bool PacketReceiver::registerListenerForTypes(PacketTypeList types, QObject* lis
     
     // Partition types based on whether they are sourced or not (non sourced in front)
     auto middle = std::partition(std::begin(types), std::end(types), [](PacketType type) {
-        return NON_SOURCED_PACKETS.contains(type);
+        return PacketTypeEnum::getNonSourcedPackets().contains(type);
     });
     
     QMetaMethod nonSourcedMethod, sourcedMethod;
@@ -123,7 +123,7 @@ QMetaMethod PacketReceiver::matchingMethodForListener(PacketType type, QObject*
         SIGNATURE_TEMPLATE.arg(slot, NON_SOURCED_MESSAGE_LISTENER_PARAMETERS)
     };
 
-    if (!NON_SOURCED_PACKETS.contains(type)) {
+    if (!PacketTypeEnum::getNonSourcedPackets().contains(type)) {
         static const QString SOURCED_MESSAGE_LISTENER_PARAMETERS = "QSharedPointer<ReceivedMessage>,QSharedPointer<Node>";
         static const QString TYPEDEF_SOURCED_MESSAGE_LISTENER_PARAMETERS = "QSharedPointer<ReceivedMessage>,SharedNodePointer";
 
diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp
index d2500196d9..241ccaf5d6 100644
--- a/libraries/networking/src/udt/PacketHeaders.cpp
+++ b/libraries/networking/src/udt/PacketHeaders.cpp
@@ -22,38 +22,6 @@
 Q_DECLARE_METATYPE(PacketType);
 int packetTypeMetaTypeId = qRegisterMetaType<PacketType>();
 
-const QSet<PacketType> NON_VERIFIED_PACKETS = QSet<PacketType>()
-    << PacketType::NodeJsonStats << PacketType::EntityQuery
-    << PacketType::OctreeDataNack << PacketType::EntityEditNack
-    << PacketType::DomainListRequest << PacketType::StopNode
-    << PacketType::DomainDisconnectRequest << PacketType::UsernameFromIDRequest
-    << PacketType::NodeKickRequest << PacketType::NodeMuteRequest;
-
-const QSet<PacketType> NON_SOURCED_PACKETS = QSet<PacketType>()
-    << PacketType::StunResponse << PacketType::CreateAssignment << PacketType::RequestAssignment
-    << PacketType::DomainServerRequireDTLS << PacketType::DomainConnectRequest
-    << PacketType::DomainList << PacketType::DomainConnectionDenied
-    << PacketType::DomainServerPathQuery << PacketType::DomainServerPathResponse
-    << PacketType::DomainServerAddedNode << PacketType::DomainServerConnectionToken
-    << PacketType::DomainSettingsRequest << PacketType::DomainSettings
-    << PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat
-    << PacketType::ICEServerHeartbeatACK << PacketType::ICEPing << PacketType::ICEPingReply
-    << PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode
-    << PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply << PacketType::OctreeFileReplacement
-    << PacketType::ReplicatedMicrophoneAudioNoEcho << PacketType::ReplicatedMicrophoneAudioWithEcho
-    << PacketType::ReplicatedInjectAudio << PacketType::ReplicatedSilentAudioFrame
-    << PacketType::ReplicatedAvatarIdentity << PacketType::ReplicatedKillAvatar << PacketType::ReplicatedBulkAvatarData;
-
-const QHash<PacketType, PacketType> REPLICATED_PACKET_MAPPING {
-    { PacketType::MicrophoneAudioNoEcho, PacketType::ReplicatedMicrophoneAudioNoEcho },
-    { PacketType::MicrophoneAudioWithEcho, PacketType::ReplicatedMicrophoneAudioWithEcho },
-    { PacketType::InjectAudio, PacketType::ReplicatedInjectAudio },
-    { PacketType::SilentAudioFrame, PacketType::ReplicatedSilentAudioFrame },
-    { PacketType::AvatarIdentity, PacketType::ReplicatedAvatarIdentity },
-    { PacketType::KillAvatar, PacketType::ReplicatedKillAvatar },
-    { PacketType::BulkAvatarData, PacketType::ReplicatedBulkAvatarData }
-};
-
 PacketVersion versionForPacketType(PacketType packetType) {
     switch (packetType) {
         case PacketType::DomainList:
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index cb3db791b4..e2304e62f7 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -123,19 +123,58 @@ public:
         ReplicatedBulkAvatarData,
         NUM_PACKET_TYPE
     };
+
+    const static QHash<PacketTypeEnum::Value, PacketTypeEnum::Value> getReplicatedPacketMapping() {
+        const static QHash<PacketTypeEnum::Value, PacketTypeEnum::Value> REPLICATED_PACKET_MAPPING {
+            { PacketTypeEnum::Value::MicrophoneAudioNoEcho, PacketTypeEnum::Value::ReplicatedMicrophoneAudioNoEcho },
+            { PacketTypeEnum::Value::MicrophoneAudioWithEcho, PacketTypeEnum::Value::ReplicatedMicrophoneAudioWithEcho },
+            { PacketTypeEnum::Value::InjectAudio, PacketTypeEnum::Value::ReplicatedInjectAudio },
+            { PacketTypeEnum::Value::SilentAudioFrame, PacketTypeEnum::Value::ReplicatedSilentAudioFrame },
+            { PacketTypeEnum::Value::AvatarIdentity, PacketTypeEnum::Value::ReplicatedAvatarIdentity },
+            { PacketTypeEnum::Value::KillAvatar, PacketTypeEnum::Value::ReplicatedKillAvatar },
+            { PacketTypeEnum::Value::BulkAvatarData, PacketTypeEnum::Value::ReplicatedBulkAvatarData }
+        };
+        return REPLICATED_PACKET_MAPPING;
+    }
+
+    const static QSet<PacketTypeEnum::Value> getNonVerifiedPackets() {
+        const static QSet<PacketTypeEnum::Value> NON_VERIFIED_PACKETS = QSet<PacketTypeEnum::Value>()
+            << PacketTypeEnum::Value::NodeJsonStats << PacketTypeEnum::Value::EntityQuery
+            << PacketTypeEnum::Value::OctreeDataNack << PacketTypeEnum::Value::EntityEditNack
+            << PacketTypeEnum::Value::DomainListRequest << PacketTypeEnum::Value::StopNode
+            << PacketTypeEnum::Value::DomainDisconnectRequest << PacketTypeEnum::Value::UsernameFromIDRequest
+            << PacketTypeEnum::Value::NodeKickRequest << PacketTypeEnum::Value::NodeMuteRequest;
+        return NON_VERIFIED_PACKETS;
+    }
+
+    const static QSet<PacketTypeEnum::Value> getNonSourcedPackets() {
+        const static QSet<PacketTypeEnum::Value> NON_SOURCED_PACKETS = QSet<PacketTypeEnum::Value>()
+            << PacketTypeEnum::Value::StunResponse << PacketTypeEnum::Value::CreateAssignment
+            << PacketTypeEnum::Value::RequestAssignment << PacketTypeEnum::Value::DomainServerRequireDTLS
+            << PacketTypeEnum::Value::DomainConnectRequest << PacketTypeEnum::Value::DomainList
+            << PacketTypeEnum::Value::DomainConnectionDenied << PacketTypeEnum::Value::DomainServerPathQuery
+            << PacketTypeEnum::Value::DomainServerPathResponse << PacketTypeEnum::Value::DomainServerAddedNode
+            << PacketTypeEnum::Value::DomainServerConnectionToken << PacketTypeEnum::Value::DomainSettingsRequest
+            << PacketTypeEnum::Value::DomainSettings << PacketTypeEnum::Value::ICEServerPeerInformation
+            << PacketTypeEnum::Value::ICEServerQuery << PacketTypeEnum::Value::ICEServerHeartbeat
+            << PacketTypeEnum::Value::ICEServerHeartbeatACK << PacketTypeEnum::Value::ICEPing
+            << PacketTypeEnum::Value::ICEPingReply << PacketTypeEnum::Value::ICEServerHeartbeatDenied
+            << PacketTypeEnum::Value::AssignmentClientStatus << PacketTypeEnum::Value::StopNode
+            << PacketTypeEnum::Value::DomainServerRemovedNode << PacketTypeEnum::Value::UsernameFromIDReply
+            << PacketTypeEnum::Value::OctreeFileReplacement << PacketTypeEnum::Value::ReplicatedMicrophoneAudioNoEcho
+            << PacketTypeEnum::Value::ReplicatedMicrophoneAudioWithEcho << PacketTypeEnum::Value::ReplicatedInjectAudio
+            << PacketTypeEnum::Value::ReplicatedSilentAudioFrame << PacketTypeEnum::Value::ReplicatedAvatarIdentity
+            << PacketTypeEnum::Value::ReplicatedKillAvatar << PacketTypeEnum::Value::ReplicatedBulkAvatarData;
+        return NON_SOURCED_PACKETS;
+    }
 };
 
 using PacketType = PacketTypeEnum::Value;
 
-extern const QHash<PacketType, PacketType> REPLICATED_PACKET_MAPPING;
-
 const int NUM_BYTES_MD5_HASH = 16;
 
 typedef char PacketVersion;
 
-extern const QSet<PacketType> NON_VERIFIED_PACKETS;
-extern const QSet<PacketType> NON_SOURCED_PACKETS;
-
 PacketVersion versionForPacketType(PacketType packetType);
 QByteArray protocolVersionsSignature(); /// returns a unqiue signature for all the current protocols
 QString protocolVersionsSignatureBase64();
diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp
index 747c72c08e..20c72159c4 100644
--- a/libraries/plugins/src/plugins/DisplayPlugin.cpp
+++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp
@@ -18,6 +18,19 @@ void DisplayPlugin::incrementPresentCount() {
 
     ++_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);
+    {
+        QMutexLocker locker(&_presentMutex);
+        _presentCondition.wakeAll();
+    }
+
+    emit presented(_presentedFrameIndex);
 }
+
+void DisplayPlugin::waitForPresent() {
+    QMutexLocker locker(&_presentMutex);
+    while (isActive()) {
+        if (_presentCondition.wait(&_presentMutex, MSECS_PER_SECOND)) {
+            break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h
index 398e586ef1..c63db81405 100644
--- a/libraries/plugins/src/plugins/DisplayPlugin.h
+++ b/libraries/plugins/src/plugins/DisplayPlugin.h
@@ -17,6 +17,8 @@
 #include <QtCore/QPoint>
 #include <QtCore/QElapsedTimer>
 #include <QtCore/QJsonObject>
+#include <QtCore/QMutex>
+#include <QtCore/QWaitCondition>
 
 #include <GLMHelpers.h>
 #include <RegisteredMetaTypes.h>
@@ -116,10 +118,6 @@ class DisplayPlugin : public Plugin, public HmdDisplay {
     Q_OBJECT
     using Parent = Plugin;
 public:
-    enum Event {
-        Present = QEvent::User + 1
-    };
-
     virtual int getRequiredThreadCount() const { return 0; }
     virtual bool isHmd() const { return false; }
     virtual int getHmdScreen() const { return -1; }
@@ -203,12 +201,15 @@ public:
 
     virtual void cycleDebugOutput() {}
 
+    void waitForPresent();
+
     static const QString& MENU_PATH();
 
 
 signals:
     void recommendedFramebufferSizeChanged(const QSize& size);
     void resetSensorsRequested();
+    void presented(quint32 frame);
 
 protected:
     void incrementPresentCount();
@@ -216,6 +217,8 @@ protected:
     gpu::ContextPointer _gpuContext;
 
 private:
+    QMutex _presentMutex;
+    QWaitCondition _presentCondition;
     std::atomic<uint32_t> _presentedFrameIndex;
     mutable std::mutex _paintDelayMutex;
     QElapsedTimer _paintDelayTimer;
diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h
index d5b5440c32..6a91081c95 100644
--- a/libraries/render/src/render/Args.h
+++ b/libraries/render/src/render/Args.h
@@ -77,7 +77,6 @@ namespace render {
         Args() {}
 
         Args(const gpu::ContextPointer& context,
-            QSharedPointer<QObject> renderData = QSharedPointer<QObject>(nullptr),
             float sizeScale = 1.0f,
             int boundaryLevelAdjust = 0,
             RenderMode renderMode = DEFAULT_RENDER_MODE,
@@ -85,7 +84,6 @@ namespace render {
             DebugFlags debugFlags = RENDER_DEBUG_NONE,
             gpu::Batch* batch = nullptr) :
             _context(context),
-            _renderData(renderData),
             _sizeScale(sizeScale),
             _boundaryLevelAdjust(boundaryLevelAdjust),
             _renderMode(renderMode),
@@ -110,7 +108,6 @@ namespace render {
         std::shared_ptr<gpu::Context> _context;
         std::shared_ptr<gpu::Framebuffer> _blitFramebuffer;
         std::shared_ptr<render::ShapePipeline> _shapePipeline;
-        QSharedPointer<QObject> _renderData;
         std::stack<ViewFrustum> _viewFrustums;
         glm::ivec4 _viewport { 0.0f, 0.0f, 1.0f, 1.0f };
         glm::vec3 _boomOffset { 0.0f, 0.0f, 1.0f };
diff --git a/libraries/script-engine/src/FileScriptingInterface.cpp b/libraries/script-engine/src/FileScriptingInterface.cpp
index 30d0a3a201..5f2460be78 100644
--- a/libraries/script-engine/src/FileScriptingInterface.cpp
+++ b/libraries/script-engine/src/FileScriptingInterface.cpp
@@ -32,26 +32,54 @@ FileScriptingInterface::FileScriptingInterface(QObject* parent) : QObject(parent
     // nothing for now
 }
 
-void FileScriptingInterface::runUnzip(QString path, QUrl url, bool autoAdd) {
+void FileScriptingInterface::runUnzip(QString path, QUrl url, bool autoAdd, bool isZip) {
     qCDebug(scriptengine) << "Url that was downloaded: " + url.toString();
     qCDebug(scriptengine) << "Path where download is saved: " + path;
     QString fileName = "/" + path.section("/", -1);
     QString tempDir = path;
-    tempDir.remove(fileName);
+    if (!isZip) {
+        tempDir.remove(fileName);
+    } else {
+        QTemporaryDir zipTemp;
+        tempDir = zipTemp.path();
+        path.remove("file:///");
+    }
+    
     qCDebug(scriptengine) << "Temporary directory at: " + tempDir;
     if (!isTempDir(tempDir)) {
         qCDebug(scriptengine) << "Temporary directory mismatch; risk of losing files";
         return;
     }
 
-    QString file = unzipFile(path, tempDir);
-    QString filename = QUrl::fromLocalFile(file).toString();
-    if (file != "") {
+    QStringList fileList = unzipFile(path, tempDir);
+    QString filename = QUrl::fromLocalFile(fileList.first()).toString();
+    
+    if (filename != "") {
         qCDebug(scriptengine) << "File to upload: " + filename;
     } else {
         qCDebug(scriptengine) << "Unzip failed";
     }
-    emit unzipResult(path, filename, autoAdd);
+    emit unzipResult(path, fileList, autoAdd, isZip);
+
+}
+
+QStringList FileScriptingInterface::unzipFile(QString path, QString tempDir) {
+
+    QDir dir(path);
+    QString dirName = dir.path();
+    qCDebug(scriptengine) << "Directory to unzip: " << dirName;
+    QString target = tempDir + "/model_repo";
+    QStringList list = JlCompress::extractDir(dirName, target);
+
+    qCDebug(scriptengine) << list;
+
+    if (!list.isEmpty()) {
+        return list;
+    } else {
+        qCDebug(scriptengine) << "Extraction failed";
+        return list;
+    }
+
 }
 
 // fix to check that we are only referring to a temporary directory
@@ -92,24 +120,6 @@ void FileScriptingInterface::downloadZip(QString path, const QString link) {
     request->send();
 }
 
-QString FileScriptingInterface::unzipFile(QString path, QString tempDir) {
-
-    QDir dir(path);
-    QString dirName = dir.path();
-    QString target = tempDir + "/model_repo";
-    QStringList list = JlCompress::extractDir(dirName, target);
-
-    qCDebug(scriptengine) << list;
-
-    if (!list.isEmpty()) {
-        return list.front();
-    } else {
-        qCDebug(scriptengine) << "Extraction failed";
-        return "";
-    }
-
-}
-
 // this function is not in use
 void FileScriptingInterface::recursiveFileScan(QFileInfo file, QString* dirName) {
     /*if (!file.isDir()) {
diff --git a/libraries/script-engine/src/FileScriptingInterface.h b/libraries/script-engine/src/FileScriptingInterface.h
index 5e9a6029e8..4069e7cc78 100644
--- a/libraries/script-engine/src/FileScriptingInterface.h
+++ b/libraries/script-engine/src/FileScriptingInterface.h
@@ -24,15 +24,15 @@ public:
 
 public slots:
     QString convertUrlToPath(QUrl url);
-    void runUnzip(QString path, QUrl url, bool autoAdd);
+    void runUnzip(QString path, QUrl url, bool autoAdd, bool isZip);
     QString getTempDir();
 
 signals:
-    void unzipResult(QString zipFile, QString unzipFile, bool autoAdd);
+    void unzipResult(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip);
 
 private:
     bool isTempDir(QString tempDir);
-    QString unzipFile(QString path, QString tempDir);
+    QStringList unzipFile(QString path, QString tempDir);
     void recursiveFileScan(QFileInfo file, QString* dirName);
     void downloadZip(QString path, const QString link);
 
diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp
index f853240fe3..4ae907eb3b 100644
--- a/libraries/shared/src/GeometryUtil.cpp
+++ b/libraries/shared/src/GeometryUtil.cpp
@@ -605,3 +605,55 @@ float coneSphereAngle(const glm::vec3& coneCenter, const glm::vec3& coneDirectio
 
     return glm::max(0.0f, theta - phi);
 }
+
+// given a set of points, compute a best fit plane that passes as close as possible through all the points.
+// http://www.ilikebigbits.com/blog/2015/3/2/plane-from-points
+bool findPlaneFromPoints(const glm::vec3* points, size_t numPoints, glm::vec3& planeNormalOut, glm::vec3& pointOnPlaneOut) {
+    if (numPoints < 3) {
+        return false;
+    }
+    glm::vec3 sum;
+    for (size_t i = 0; i < numPoints; i++) {
+        sum += points[i];
+    }
+    glm::vec3 centroid = sum * (1.0f / (float)numPoints);
+    float xx = 0.0f, xy = 0.0f, xz = 0.0f;
+    float yy = 0.0f, yz = 0.0f, zz = 0.0f;
+
+    for (size_t i = 0; i < numPoints; i++) {
+        glm::vec3 r = points[i] - centroid;
+        xx += r.x * r.x;
+        xy += r.x * r.y;
+        xz += r.x * r.z;
+        yy += r.y * r.y;
+        yz += r.y * r.z;
+        zz += r.z * r.z;
+    }
+
+    float det_x = yy * zz - yz * yz;
+    float det_y = xx * zz - xz * xz;
+    float det_z = xx * yy - xy * xy;
+    float det_max = std::max(std::max(det_x, det_y), det_z);
+
+    if (det_max == 0.0f) {
+        return false; // The points don't span a plane
+    }
+
+    glm::vec3 dir;
+    if (det_max == det_x) {
+        float a = (xz * yz - xy * zz) / det_x;
+        float b = (xy * yz - xz * yy) / det_x;
+        dir = glm::vec3(1.0f, a, b);
+    } else if (det_max == det_y) {
+        float a = (yz * xz - xy * zz) / det_y;
+        float b = (xy * xz - yz * xx) / det_y;
+        dir = glm::vec3(a, 1.0f, b);
+    } else {
+        float a = (yz * xy - xz * yy) / det_z;
+        float b = (xz * xy - yz * xx) / det_z;
+        dir = glm::vec3(a, b, 1.0f);
+    }
+    pointOnPlaneOut = centroid;
+    planeNormalOut = glm::normalize(dir);
+    return true;
+}
diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h
index 857d423896..a5ee67748b 100644
--- a/libraries/shared/src/GeometryUtil.h
+++ b/libraries/shared/src/GeometryUtil.h
@@ -163,5 +163,7 @@ private:
     static void copyCleanArray(int& lengthA, glm::vec2* vertexArrayA, int& lengthB, glm::vec2* vertexArrayB);
 };
 
+// given a set of points, compute a best fit plane that passes as close as possible through all the points.
+bool findPlaneFromPoints(const glm::vec3* points, size_t numPoints, glm::vec3& planeNormalOut, glm::vec3& pointOnPlaneOut);
 
 #endif // hifi_GeometryUtil_h
diff --git a/libraries/shared/src/ThreadHelpers.cpp b/libraries/shared/src/ThreadHelpers.cpp
index 8f3d16a577..654b8e0252 100644
--- a/libraries/shared/src/ThreadHelpers.cpp
+++ b/libraries/shared/src/ThreadHelpers.cpp
@@ -10,29 +10,71 @@
 
 #include <QtCore/QDebug>
 
+// Support for viewing the thread name in the debugger.  
+// Note, Qt actually does this for you but only in debug builds
+// Code from https://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx
+// and matches logic in `qt_set_thread_name` in qthread_win.cpp
+#ifdef Q_OS_WIN
+#include <qt_windows.h>
+#pragma pack(push,8)  
+struct THREADNAME_INFO {
+    DWORD dwType; // Must be 0x1000.  
+    LPCSTR szName; // Pointer to name (in user addr space).  
+    DWORD dwThreadID; // Thread ID (-1=caller thread).  
+    DWORD dwFlags; // Reserved for future use, must be zero.  
+};
+#pragma pack(pop)  
+#endif
+
+void setThreadName(const std::string& name) {
+#ifdef Q_OS_WIN
+    static const DWORD MS_VC_EXCEPTION = 0x406D1388;
+    THREADNAME_INFO info{ 0x1000, name.c_str(), (DWORD)-1, 0 };
+    __try {
+        RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
+    } __except (EXCEPTION_EXECUTE_HANDLER) { }
+#endif
+}
+
+void moveToNewNamedThread(QObject* object, const QString& name, std::function<void(QThread*)> preStartCallback, std::function<void()> startCallback, QThread::Priority priority) {
+    Q_ASSERT(QThread::currentThread() == object->thread());
+
+    // Create the target thread
+    QThread* thread = new QThread();
+    thread->setObjectName(name);
+
+    // Execute any additional work to do before the thread starts like moving members to the target thread.
+    // This is required as QObject::moveToThread isn't virutal, so we can't override it on objects that contain
+    // an OpenGLContext and ensure that the context moves to the target thread as well.  
+    preStartCallback(thread);
+
+    // Link the in-thread initialization code
+    QObject::connect(thread, &QThread::started, [name, startCallback] { 
+        if (!name.isEmpty()) {
+            // Make it easy to spot our thread processes inside the debugger
+            setThreadName("Hifi_" + name.toStdString());
+        }
+        startCallback();
+    });
+
+    // Make sure the thread will be destroyed and cleaned up.  The assumption here is that the incoming object 
+    // will be destroyed and the thread will quit when that occurs.  
+    QObject::connect(object, &QObject::destroyed, thread, &QThread::quit);
+    // When the thread itself stops running, it should also be deleted.
+    QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
+
+    // put the object on the thread
+    object->moveToThread(thread);
+    thread->start();
+    if (priority != QThread::InheritPriority) {
+        thread->setPriority(priority);
+    }
+}
 
 void moveToNewNamedThread(QObject* object, const QString& name, std::function<void()> startCallback, QThread::Priority priority) {
-     Q_ASSERT(QThread::currentThread() == object->thread());
-     // setup a thread for the NodeList and its PacketReceiver
-     QThread* thread = new QThread();
-     thread->setObjectName(name);
-
-     QString tempName = name;
-     QObject::connect(thread, &QThread::started, [startCallback] {
-         startCallback();
-     });
-     // Make sure the thread will be destroyed and cleaned up
-     QObject::connect(object, &QObject::destroyed, thread, &QThread::quit);
-     QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
-
-     // put the object on the thread
-     object->moveToThread(thread);
-     thread->start();
-     if (priority != QThread::InheritPriority) {
-         thread->setPriority(priority);
-     }
+    moveToNewNamedThread(object, name, [](QThread*){}, startCallback, priority);
 }
 
 void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority) {
-    moveToNewNamedThread(object, name, [] {}, priority);
+    moveToNewNamedThread(object, name, [](QThread*){}, []{}, priority);
 }
diff --git a/libraries/shared/src/ThreadHelpers.h b/libraries/shared/src/ThreadHelpers.h
index 6e024f787a..d236344dc5 100644
--- a/libraries/shared/src/ThreadHelpers.h
+++ b/libraries/shared/src/ThreadHelpers.h
@@ -32,8 +32,17 @@ void withLock(QMutex& lock, F function) {
     function();
 }
 
-void moveToNewNamedThread(QObject* object, const QString& name, std::function<void()> startCallback, QThread::Priority priority = QThread::InheritPriority);
-void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority = QThread::InheritPriority);
+void moveToNewNamedThread(QObject* object, const QString& name, 
+    std::function<void(QThread*)> preStartCallback, 
+    std::function<void()> startCallback, 
+    QThread::Priority priority = QThread::InheritPriority);
+
+void moveToNewNamedThread(QObject* object, const QString& name, 
+    std::function<void()> startCallback, 
+    QThread::Priority priority = QThread::InheritPriority);
+
+void moveToNewNamedThread(QObject* object, const QString& name, 
+    QThread::Priority priority = QThread::InheritPriority);
 
 class ConditionalGuard {
 public:
diff --git a/libraries/shared/src/shared/QtHelpers.cpp b/libraries/shared/src/shared/QtHelpers.cpp
index 1ce1c3e07c..3e8c6d57ed 100644
--- a/libraries/shared/src/shared/QtHelpers.cpp
+++ b/libraries/shared/src/shared/QtHelpers.cpp
@@ -11,11 +11,24 @@
 #include <QtCore/QThread>
 #include <QtCore/QCoreApplication>
 #include <QtCore/QLoggingCategory>
+#include <QtCore/QReadWriteLock>
 
+#include "../Profile.h"
 Q_LOGGING_CATEGORY(thread_safety, "hifi.thread_safety")
 
 namespace hifi { namespace qt {
 
+static QHash<QThread*, QString> threadHash;
+static QReadWriteLock threadHashLock;
+
+void addBlockingForbiddenThread(const QString& name, QThread* thread) {
+    if (!thread) {
+        thread = QThread::currentThread();
+    }
+    QWriteLocker locker(&threadHashLock);
+    threadHash[thread] = name;
+}
+
 bool blockingInvokeMethod(
     const char* function,
     QObject *obj, const char *member,
@@ -30,9 +43,23 @@ bool blockingInvokeMethod(
     QGenericArgument val7,
     QGenericArgument val8,
     QGenericArgument val9) {
-    if (QThread::currentThread() == qApp->thread()) {
+    auto currentThread = QThread::currentThread();
+    if (currentThread == qApp->thread()) {
         qCWarning(thread_safety) << "BlockingQueuedConnection invoked on main thread from " << function;
+        return QMetaObject::invokeMethod(obj, member,
+            Qt::BlockingQueuedConnection, ret, val0, val1, val2, val3, val4, val5, val6, val7, val8, val9);
+    } 
+
+    {
+        QReadLocker locker(&threadHashLock);
+        for (const auto& thread : threadHash.keys()) {
+            if (currentThread == thread) {
+                qCWarning(thread_safety) << "BlockingQueuedConnection invoked on forbidden thread " << threadHash[thread];
+            }
+        }
     }
+
+    PROFILE_RANGE(app, function);
     return QMetaObject::invokeMethod(obj, member,
             Qt::BlockingQueuedConnection, ret, val0, val1, val2, val3, val4, val5, val6, val7, val8, val9);
 }
diff --git a/libraries/shared/src/shared/QtHelpers.h b/libraries/shared/src/shared/QtHelpers.h
index 5da65a378f..2133119324 100644
--- a/libraries/shared/src/shared/QtHelpers.h
+++ b/libraries/shared/src/shared/QtHelpers.h
@@ -14,6 +14,7 @@
 
 
 namespace hifi { namespace qt {
+void addBlockingForbiddenThread(const QString& name, QThread* thread = nullptr);
 
 bool blockingInvokeMethod(
     const char* function,
diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp
index 0fd32b42e6..984d743ebf 100644
--- a/libraries/ui/src/ui/TabletScriptingInterface.cpp
+++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp
@@ -28,11 +28,9 @@ const QString SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system";
 const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
 
 TabletScriptingInterface::TabletScriptingInterface() {
-    qCDebug(uiLogging) << "Building tablet scripting interface";
 }
 
 TabletScriptingInterface::~TabletScriptingInterface() {
-    qCDebug(uiLogging) << "Destroying tablet scripting interface";
 }
 
 ToolbarProxy* TabletScriptingInterface::getSystemToolbarProxy() {
@@ -191,7 +189,6 @@ TabletProxy::TabletProxy(QObject* parent, const QString& name) : QObject(parent)
 }
 
 TabletProxy::~TabletProxy() {
-    qCDebug(uiLogging) << "Destroying tablet proxy " << _name;
     if (QThread::currentThread() != thread()) {
         qCWarning(uiLogging) << "Destroying tablet proxy on wrong thread" << _name;
     }
@@ -846,7 +843,6 @@ TabletButtonProxy::TabletButtonProxy(const QVariantMap& properties) :
 }
 
 TabletButtonProxy::~TabletButtonProxy() {
-    qCDebug(uiLogging) << "Destroying tablet button proxy " ;
     if (QThread::currentThread() != thread()) {
         qCWarning(uiLogging) << "Destroying tablet button proxy on wrong thread";
     }
diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt
index 6a95ef6d76..7878ae2d7e 100644
--- a/plugins/openvr/CMakeLists.txt
+++ b/plugins/openvr/CMakeLists.txt
@@ -18,6 +18,7 @@ if (WIN32)
     include_hifi_library_headers(octree)
 
     add_dependency_external_projects(OpenVR)
+
     find_package(OpenVR REQUIRED)
     target_include_directories(${TARGET_NAME} PRIVATE ${OPENVR_INCLUDE_DIRS})
     target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES})
diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp
index 07b3b2f73d..5a1c23839e 100644
--- a/plugins/openvr/src/ViveControllerManager.cpp
+++ b/plugins/openvr/src/ViveControllerManager.cpp
@@ -35,7 +35,6 @@
 #include <Plugins/InputConfiguration.h>
 #include <controllers/StandardControls.h>
 
-
 extern PoseData _nextSimPoseData;
 
 vr::IVRSystem* acquireOpenVrSystem();
@@ -168,6 +167,7 @@ void ViveControllerManager::setConfigurationSettings(const QJsonObject configura
             }
         }
         _inputDevice->configureCalibrationSettings(configurationSettings);
+        saveSettings();
     }
 }
 
@@ -188,6 +188,8 @@ QString ViveControllerManager::configurationLayout() {
 bool ViveControllerManager::activate() {
     InputPlugin::activate();
 
+    loadSettings();
+
     if (!_system) {
         _system = acquireOpenVrSystem();
     }
@@ -230,6 +232,8 @@ void ViveControllerManager::deactivate() {
     auto userInputMapper = DependencyManager::get<controller::UserInputMapper>();
     userInputMapper->removeDevice(_inputDevice->_deviceID);
     _registeredWithInputMapper = false;
+
+    saveSettings();
 }
 
 bool ViveControllerManager::isHeadControllerMounted() const {
@@ -282,7 +286,38 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu
     }
 }
 
-ViveControllerManager::InputDevice::InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) {
+void ViveControllerManager::loadSettings() {
+    Settings settings;
+    QString nameString = getName();
+    settings.beginGroup(nameString);
+    {
+        if (_inputDevice) {
+            const double DEFAULT_ARM_CIRCUMFERENCE = 0.33;
+            const double DEFAULT_SHOULDER_WIDTH = 0.48;
+            _inputDevice->_armCircumference = settings.value("armCircumference", QVariant(DEFAULT_ARM_CIRCUMFERENCE)).toDouble();
+            _inputDevice->_shoulderWidth = settings.value("shoulderWidth", QVariant(DEFAULT_SHOULDER_WIDTH)).toDouble();
+        }
+    }
+    settings.endGroup();
+}
+
+void ViveControllerManager::saveSettings() const {
+    Settings settings;
+    QString nameString = getName();
+    settings.beginGroup(nameString);
+    {
+        if (_inputDevice) {
+            settings.setValue(QString("armCircumference"), _inputDevice->_armCircumference);
+            settings.setValue(QString("shoulderWidth"), _inputDevice->_shoulderWidth);
+        }
+    }
+    settings.endGroup();
+}
+
+
+ViveControllerManager::InputDevice::InputDevice(vr::IVRSystem*& system) :
+    controller::InputDevice("Vive"),
+    _system(system) {
 
     _configStringMap[Config::None] = QString("None");
     _configStringMap[Config::Feet] = QString("Feet");
@@ -371,6 +406,9 @@ void ViveControllerManager::InputDevice::calibrateFromUI(const controller::Input
     }
 }
 
+static const float CM_TO_M = 0.01f;
+static const float M_TO_CM = 100.0f;
+
 void ViveControllerManager::InputDevice::configureCalibrationSettings(const QJsonObject configurationSettings) {
     Locker locker(_lock);
     if (!configurationSettings.empty()) {
@@ -384,8 +422,8 @@ void ViveControllerManager::InputDevice::configureCalibrationSettings(const QJso
                 bool overrideHead = headObject["override"].toBool();
                 if (overrideHead) {
                     _headConfig = HeadConfig::Puck;
-                    _headPuckYOffset = headObject["Y"].toDouble();
-                    _headPuckZOffset = headObject["Z"].toDouble();
+                    _headPuckYOffset = headObject["Y"].toDouble() * CM_TO_M;
+                    _headPuckZOffset = headObject["Z"].toDouble() * CM_TO_M;
                 } else {
                     _headConfig = HeadConfig::HMD;
                 }
@@ -394,11 +432,15 @@ void ViveControllerManager::InputDevice::configureCalibrationSettings(const QJso
                 bool overrideHands = handsObject["override"].toBool();
                 if (overrideHands) {
                     _handConfig = HandConfig::Pucks;
-                    _handPuckYOffset = handsObject["Y"].toDouble();
-                    _handPuckZOffset = handsObject["Z"].toDouble();
+                    _handPuckYOffset = handsObject["Y"].toDouble() * CM_TO_M;
+                    _handPuckZOffset = handsObject["Z"].toDouble() * CM_TO_M;
                 } else {
                     _handConfig = HandConfig::HandController;
                 }
+            } else if (iter.key() == "armCircumference") {
+                _armCircumference = (float)iter.value().toDouble() * CM_TO_M;
+            } else if (iter.key() == "shoulderWidth") {
+                _shoulderWidth = (float)iter.value().toDouble() * CM_TO_M;
             }
             iter++;
         }
@@ -417,6 +459,8 @@ QJsonObject ViveControllerManager::InputDevice::configurationSettings() {
     configurationSettings["HMDHead"] = (_headConfig == HeadConfig::HMD);
     configurationSettings["handController"] = (_handConfig == HandConfig::HandController);
     configurationSettings["puckCount"] = (int)_validTrackedObjects.size();
+    configurationSettings["armCircumference"] = (double)_armCircumference * M_TO_CM;
+    configurationSettings["shoulderWidth"] = (double)_shoulderWidth * M_TO_CM;
     return configurationSettings;
 }
 
@@ -534,7 +578,7 @@ void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibr
     }
 }
 
-bool ViveControllerManager::InputDevice::configureHands(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
+bool ViveControllerManager::InputDevice::configureHands(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
     std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksXPosition);
     int puckCount = (int)_validTrackedObjects.size();
     if (_handConfig == HandConfig::Pucks && puckCount >= MIN_PUCK_COUNT) {
@@ -569,7 +613,7 @@ bool ViveControllerManager::InputDevice::configureHands(glm::mat4& defaultToRefe
     return false;
 }
 
-bool ViveControllerManager::InputDevice::configureHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
+bool ViveControllerManager::InputDevice::configureHead(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
     std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksYPosition);
     int puckCount = (int)_validTrackedObjects.size();
     if (_headConfig == HeadConfig::Puck && puckCount >= MIN_HEAD) {
@@ -583,7 +627,7 @@ bool ViveControllerManager::InputDevice::configureHead(glm::mat4& defaultToRefer
     return false;
 }
 
-bool ViveControllerManager::InputDevice::configureBody(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
+bool ViveControllerManager::InputDevice::configureBody(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
     std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksYPosition);
     int puckCount = (int)_validTrackedObjects.size();
     glm::vec3 headXAxis = getReferenceHeadXAxis(defaultToReferenceMat, inputCalibration.defaultHeadMat);
@@ -624,7 +668,8 @@ bool ViveControllerManager::InputDevice::configureBody(glm::mat4& defaultToRefer
 
 void ViveControllerManager::InputDevice::uncalibrate() {
     _config = Config::None;
-    _pucksOffset.clear();
+    _pucksPostOffset.clear();
+    _pucksPreOffset.clear();
     _jointToPuckMap.clear();
     _calibrated = false;
     _overrideHead = false;
@@ -654,10 +699,17 @@ controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(int joi
     if (puck != _jointToPuckMap.end()) {
         uint32_t puckIndex = puck->second;
         auto puckPose = _poseStateMap.find(puckIndex);
-        auto puckOffset = _pucksOffset.find(puckIndex);
+        auto puckPostOffset = _pucksPostOffset.find(puckIndex);
+        auto puckPreOffset = _pucksPreOffset.find(puckIndex);
 
-        if ((puckPose != _poseStateMap.end()) && (puckOffset != _pucksOffset.end())) {
-            return puckPose->second.postTransform(puckOffset->second);
+        if (puckPose != _poseStateMap.end()) {
+            if (puckPreOffset != _pucksPreOffset.end() && puckPostOffset != _pucksPostOffset.end()) {
+                return puckPose->second.postTransform(puckPostOffset->second).transform(puckPreOffset->second);
+            } else if (puckPostOffset != _pucksPostOffset.end()) {
+                return puckPose->second.postTransform(puckPostOffset->second);
+            } else if (puckPreOffset != _pucksPreOffset.end()) {
+                return puckPose->second.transform(puckPreOffset->second);
+            }
         }
     }
     return controller::Pose();
@@ -708,7 +760,7 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u
             // pseudo buttons the depend on both of the above for-loops
             partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_X, controller::LS_Y);
             partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_X, controller::RS_Y);
-         }
+        }
     }
 }
 
@@ -942,7 +994,7 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef
     }
 }
 
-void ViveControllerManager::InputDevice::calibrateLeftHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair) {
+void ViveControllerManager::InputDevice::calibrateLeftHand(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair) {
     controller::Pose& handPose = handPair.second;
     glm::mat4 handPoseAvatarMat = createMatFromQuatAndPos(handPose.getRotation(), handPose.getTranslation());
     glm::vec3 handPoseTranslation = extractTranslation(handPoseAvatarMat);
@@ -970,10 +1022,10 @@ void ViveControllerManager::InputDevice::calibrateLeftHand(glm::mat4& defaultToR
     glm::mat4 offsetMat = createMatFromQuatAndPos(rotationOffset, translationOffset);
 
     _jointToPuckMap[controller::LEFT_HAND] = handPair.first;
-    _pucksOffset[handPair.first] = offsetMat;
+    _pucksPostOffset[handPair.first] = offsetMat;
 }
 
-void ViveControllerManager::InputDevice::calibrateRightHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair) {
+void ViveControllerManager::InputDevice::calibrateRightHand(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair) {
     controller::Pose& handPose = handPair.second;
     glm::mat4 handPoseAvatarMat = createMatFromQuatAndPos(handPose.getRotation(), handPose.getTranslation());
     glm::vec3 handPoseTranslation = extractTranslation(handPoseAvatarMat);
@@ -1001,11 +1053,11 @@ void ViveControllerManager::InputDevice::calibrateRightHand(glm::mat4& defaultTo
     glm::mat4 offsetMat = createMatFromQuatAndPos(rotationOffset, translationOffset);
 
     _jointToPuckMap[controller::RIGHT_HAND] = handPair.first;
-    _pucksOffset[handPair.first] = offsetMat;
+    _pucksPostOffset[handPair.first] = offsetMat;
 }
 
 
-void ViveControllerManager::InputDevice::calibrateFeet(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
+void ViveControllerManager::InputDevice::calibrateFeet(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
     glm::vec3 headXAxis = getReferenceHeadXAxis(defaultToReferenceMat, inputCalibration.defaultHeadMat);
     glm::vec3 headPosition = getReferenceHeadPosition(defaultToReferenceMat, inputCalibration.defaultHeadMat);
     auto& firstFoot = _validTrackedObjects[FIRST_FOOT];
@@ -1022,7 +1074,7 @@ void ViveControllerManager::InputDevice::calibrateFeet(glm::mat4& defaultToRefer
     }
 }
 
-void ViveControllerManager::InputDevice::calibrateFoot(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& footPair, bool isLeftFoot){
+void ViveControllerManager::InputDevice::calibrateFoot(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& footPair, bool isLeftFoot){
     controller::Pose footPose = footPair.second;
     glm::mat4 puckPoseAvatarMat = createMatFromQuatAndPos(footPose.getRotation(), footPose.getTranslation());
     glm::mat4 defaultFoot = isLeftFoot ? inputCalibration.defaultLeftFoot : inputCalibration.defaultRightFoot;
@@ -1037,48 +1089,96 @@ void ViveControllerManager::InputDevice::calibrateFoot(glm::mat4& defaultToRefer
 
     if (isLeftFoot) {
         _jointToPuckMap[controller::LEFT_FOOT] = footPair.first;
-        _pucksOffset[footPair.first] = finalOffset;
+        _pucksPostOffset[footPair.first] = finalOffset;
     } else {
         _jointToPuckMap[controller::RIGHT_FOOT] = footPair.first;
-        _pucksOffset[footPair.first] = finalOffset;
+        _pucksPostOffset[footPair.first] = finalOffset;
     }
 }
 
-void ViveControllerManager::InputDevice::calibrateHips(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
+void ViveControllerManager::InputDevice::calibrateHips(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
     _jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first;
-    _pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second);
+    _pucksPostOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second);
 }
 
-void ViveControllerManager::InputDevice::calibrateChest(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
+void ViveControllerManager::InputDevice::calibrateChest(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
     _jointToPuckMap[controller::SPINE2] = _validTrackedObjects[CHEST].first;
-    _pucksOffset[_validTrackedObjects[CHEST].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultSpine2, _validTrackedObjects[CHEST].second);
+    _pucksPostOffset[_validTrackedObjects[CHEST].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultSpine2, _validTrackedObjects[CHEST].second);
 }
 
-void ViveControllerManager::InputDevice::calibrateShoulders(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration,
+// y axis comes out of puck usb port/green light
+// -z axis comes out of puck center/vive logo
+static glm::vec3 computeUserShoulderPositionFromMeasurements(float armCirc, float shoulderSpan, const glm::mat4& headMat, const controller::Pose& armPuck, bool isLeftHand) {
+
+    float armRadius = armCirc / TWO_PI;
+
+    float sign = isLeftHand ? 1.0f : -1.0f;
+    float localArmX = sign * shoulderSpan / 2.0f;
+
+    controller::Pose localPuck = armPuck.transform(glm::inverse(headMat));
+    glm::mat4 localPuckMat = localPuck.getMatrix();
+    glm::vec3 localArmCenter = extractTranslation(localPuckMat) + armRadius * transformVectorFast(localPuckMat, Vectors::UNIT_Z);
+
+    return transformPoint(headMat, glm::vec3(localArmX, localArmCenter.y, localArmCenter.z));
+}
+
+void ViveControllerManager::InputDevice::calibrateShoulders(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration,
                                                             int firstShoulderIndex, int secondShoulderIndex) {
     const PuckPosePair& firstShoulder = _validTrackedObjects[firstShoulderIndex];
     const PuckPosePair& secondShoulder = _validTrackedObjects[secondShoulderIndex];
     const controller::Pose& firstShoulderPose = firstShoulder.second;
     const controller::Pose& secondShoulderPose = secondShoulder.second;
 
+    glm::mat4 refLeftArm = defaultToReferenceMat * inputCalibration.defaultLeftArm;
+    glm::mat4 refRightArm = defaultToReferenceMat * inputCalibration.defaultRightArm;
+
+    glm::mat4 userRefLeftArm = refLeftArm;
+    glm::mat4 userRefRightArm = refRightArm;
+
+    glm::mat4 headMat = defaultToReferenceMat * inputCalibration.defaultHeadMat;
+
     if (firstShoulderPose.translation.x < secondShoulderPose.translation.x) {
         _jointToPuckMap[controller::LEFT_ARM] = firstShoulder.first;
-        _pucksOffset[firstShoulder.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftArm, firstShoulder.second);
         _jointToPuckMap[controller::RIGHT_ARM] = secondShoulder.first;
-        _pucksOffset[secondShoulder.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightArm, secondShoulder.second);
+
+        glm::vec3 leftPos = computeUserShoulderPositionFromMeasurements(_armCircumference, _shoulderWidth, headMat, firstShoulderPose, true);
+        userRefLeftArm[3] = glm::vec4(leftPos, 1.0f);
+        glm::vec3 rightPos = computeUserShoulderPositionFromMeasurements(_armCircumference, _shoulderWidth, headMat, secondShoulderPose, false);
+        userRefRightArm[3] = glm::vec4(rightPos, 1.0f);
+
+        // compute the post offset from the userRefArm
+        _pucksPostOffset[firstShoulder.first] = computeOffset(Matrices::IDENTITY, userRefLeftArm, firstShoulderPose);
+        _pucksPostOffset[secondShoulder.first] = computeOffset(Matrices::IDENTITY, userRefRightArm, secondShoulderPose);
+
+        // compute the pre offset from the diff between userRefArm and refArm transforms.
+        // as an optimization we don't do a full inverse, but subtract the translations.
+        _pucksPreOffset[firstShoulder.first] = createMatFromQuatAndPos(glm::quat(), extractTranslation(userRefLeftArm) - extractTranslation(refLeftArm));
+        _pucksPreOffset[secondShoulder.first] = createMatFromQuatAndPos(glm::quat(), extractTranslation(userRefRightArm) - extractTranslation(refRightArm));
     } else {
         _jointToPuckMap[controller::LEFT_ARM] = secondShoulder.first;
-        _pucksOffset[secondShoulder.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftArm, secondShoulder.second);
         _jointToPuckMap[controller::RIGHT_ARM] = firstShoulder.first;
-        _pucksOffset[firstShoulder.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightArm, firstShoulder.second);
+
+        glm::vec3 leftPos = computeUserShoulderPositionFromMeasurements(_armCircumference, _shoulderWidth, headMat, secondShoulderPose, true);
+        userRefLeftArm[3] = glm::vec4(leftPos, 1.0f);
+        glm::vec3 rightPos = computeUserShoulderPositionFromMeasurements(_armCircumference, _shoulderWidth, headMat, firstShoulderPose, false);
+        userRefRightArm[3] = glm::vec4(rightPos, 1.0f);
+
+        // compute the post offset from the userRefArm
+        _pucksPostOffset[secondShoulder.first] = computeOffset(Matrices::IDENTITY, userRefLeftArm, secondShoulderPose);
+        _pucksPostOffset[firstShoulder.first] = computeOffset(Matrices::IDENTITY, userRefRightArm, firstShoulderPose);
+
+        // compute the pre offset from the diff between userRefArm and refArm transforms.
+        // as an optimization we don't do a full inverse, but subtract the translations.
+        _pucksPreOffset[secondShoulder.first] = createMatFromQuatAndPos(glm::quat(), extractTranslation(userRefLeftArm) - extractTranslation(refLeftArm));
+        _pucksPreOffset[firstShoulder.first] = createMatFromQuatAndPos(glm::quat(), extractTranslation(userRefRightArm) - extractTranslation(refRightArm));
     }
 }
 
-void ViveControllerManager::InputDevice::calibrateHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
+void ViveControllerManager::InputDevice::calibrateHead(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration) {
     size_t headIndex = _validTrackedObjects.size() - 1;
     const PuckPosePair& head = _validTrackedObjects[headIndex];
     _jointToPuckMap[controller::HEAD] = head.first;
-    _pucksOffset[head.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHeadMat, head.second);
+    _pucksPostOffset[head.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHeadMat, head.second);
 }
 
 QString ViveControllerManager::InputDevice::configToString(Config config) {
diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h
index a9bcc7e4e2..9a7b2cbc93 100644
--- a/plugins/openvr/src/ViveControllerManager.h
+++ b/plugins/openvr/src/ViveControllerManager.h
@@ -57,6 +57,9 @@ public:
 
     void setRenderControllers(bool renderControllers) { _renderControllers = renderControllers; }
 
+    virtual void saveSettings() const override;
+    virtual void loadSettings() override;
+
 private:
     class InputDevice : public controller::InputDevice {
     public:
@@ -93,18 +96,18 @@ private:
         void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton);
         void printDeviceTrackingResultChange(uint32_t deviceIndex);
         void setConfigFromString(const QString& value);
-        bool configureHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
-        bool configureHands(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
-        bool configureBody(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
-        void calibrateLeftHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
-        void calibrateRightHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
-        void calibrateFeet(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
-        void calibrateFoot(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& footPair, bool isLeftFoot);
-        void calibrateHips(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
-        void calibrateChest(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
-        void calibrateShoulders(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration,
+        bool configureHead(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
+        bool configureHands(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
+        bool configureBody(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
+        void calibrateLeftHand(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
+        void calibrateRightHand(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
+        void calibrateFeet(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
+        void calibrateFoot(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& footPair, bool isLeftFoot);
+        void calibrateHips(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
+        void calibrateChest(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
+        void calibrateShoulders(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration,
                                 int firstShoulderIndex, int secondShoulderIndex);
-        void calibrateHead(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
+        void calibrateHead(const glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
         void calibrateFromHandController(const controller::InputCalibrationData& inputCalibrationData);
         void calibrateFromUI(const controller::InputCalibrationData& inputCalibrationData);
         void emitCalibrationStatus();
@@ -161,7 +164,8 @@ private:
         FilteredStick _filteredRightStick;
 
         std::vector<PuckPosePair> _validTrackedObjects;
-        std::map<uint32_t, glm::mat4> _pucksOffset;
+        std::map<uint32_t, glm::mat4> _pucksPostOffset;
+        std::map<uint32_t, glm::mat4> _pucksPreOffset;
         std::map<int, uint32_t> _jointToPuckMap;
         std::map<Config, QString> _configStringMap;
         PoseData _lastSimPoseData;
@@ -181,6 +185,8 @@ private:
         float _headPuckZOffset { -0.05f };
         float _handPuckYOffset { 0.0f };
         float _handPuckZOffset { 0.0f };
+        float _armCircumference { 0.33f };
+        float _shoulderWidth { 0.48f };
         bool _triggersPressedHandled { false };
         bool _calibrated { false };
         bool _timeTilCalibrationSet { false };
diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js
index 3598b461a4..c9c3dbe493 100644
--- a/scripts/system/hmd.js
+++ b/scripts/system/hmd.js
@@ -43,17 +43,20 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
 // Independent and Entity mode make people sick; disable them in hmd.
 var desktopOnlyViews = ['Independent Mode', 'Entity Mode'];
 
+var switchToVR = "ENTER VR";
+var switchToDesktop = "EXIT VR";
+
 function onHmdChanged(isHmd) {
     HMD.closeTablet();
     if (isHmd) {
         button.editProperties({
             icon: "icons/tablet-icons/switch-desk-i.svg",
-            text: "DESKTOP"
+            text: switchToDesktop
         });
     } else {
         button.editProperties({
             icon: "icons/tablet-icons/switch-vr-i.svg",
-            text: "VR"
+            text: switchToVR
         });
     }
     desktopOnlyViews.forEach(function (view) {
@@ -70,7 +73,7 @@ function onClicked() {
 if (headset) {
     button = tablet.addButton({
         icon: HMD.active ? "icons/tablet-icons/switch-desk-i.svg" : "icons/tablet-icons/switch-vr-i.svg",
-        text: HMD.active ? "DESKTOP" : "VR",
+        text: HMD.active ? switchToDesktop : switchToVR,
         sortOrder: 2
     });
     onHmdChanged(HMD.active);
diff --git a/scripts/system/html/ChatPage.html b/scripts/system/html/ChatPage.html
index e1a3776dd5..9606eeab3e 100644
--- a/scripts/system/html/ChatPage.html
+++ b/scripts/system/html/ChatPage.html
@@ -195,7 +195,7 @@
             <div class="ChatLog" id="ChatLog"></div>
 
             <div class="ChatInput">
-              <input type="text" class="ChatInputText" id="ChatInputText" size="256"/>
+              <input type="text" class="ChatInputText" id="ChatInputText" size="256" maxlength="256" />
             </div>
 
         </div>
diff --git a/scripts/system/html/img/blocks-tile.png b/scripts/system/html/img/blocks-tile.png
new file mode 100644
index 0000000000..49de535c1c
Binary files /dev/null and b/scripts/system/html/img/blocks-tile.png differ
diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js
index 3b3d4b4937..8a8cf62008 100644
--- a/scripts/system/html/js/marketplacesInject.js
+++ b/scripts/system/html/js/marketplacesInject.js
@@ -33,8 +33,8 @@
         $("head").append(
             '<style>' +
                 '#marketplace-navigation { font-family: Arial, Helvetica, sans-serif; width: 100%; height: 50px; background: #00b4ef; position: fixed; bottom: 0; z-index: 1000; }' +
-                '#marketplace-navigation .glyph { margin-left: 20px; margin-right: 3px; font-family: sans-serif; color: #fff; font-size: 24px; line-height: 50px; }' +
-                '#marketplace-navigation .text { color: #fff; font-size: 18px; line-height: 50px; vertical-align: top; position: relative; top: 1px; }' +
+                '#marketplace-navigation .glyph { margin-left: 10px; margin-right: 3px; font-family: sans-serif; color: #fff; font-size: 24px; line-height: 50px; }' +
+                '#marketplace-navigation .text { color: #fff; font-size: 16px; line-height: 50px; vertical-align: top; position: relative; top: 1px; }' +
                 '#marketplace-navigation input#back-button { position: absolute; left: 20px; margin-top: 12px; padding-left: 0; padding-right: 5px; }' +
                 '#marketplace-navigation input#all-markets { position: absolute; right: 20px; margin-top: 12px; padding-left: 15px; padding-right: 15px; }' +
                 '#marketplace-navigation .right { position: absolute; right: 20px; }' +
@@ -57,7 +57,7 @@
         $("body").append(
             '<div id="marketplace-navigation">' +
                 (!isInitialHiFiPage ? '<input id="back-button" type="button" class="white" value="&lt; Back" />' : '') +
-                (isInitialHiFiPage ? '<span class="glyph">&#x1f6c8;</span> <span class="text">See also other marketplaces.</span>' : '') +
+                (isInitialHiFiPage ? '<span class="glyph">&#x1f6c8;</span> <span class="text">Get items from Clara.io!</span>' : '') +
                 (!isDirectoryPage ? '<input id="all-markets" type="button" class="white" value="See All Markets" />' : '') +
                 (isDirectoryPage ? '<span class="right"><span class="glyph">&#x1f6c8;</span> <span class="text">Select a marketplace to explore.</span><span>' : '') +
             '</div>'
@@ -65,7 +65,7 @@
 
         // Footer actions.
         $("#back-button").on("click", function () {
-            window.history.back();
+            (window.history.state != null) ? window.history.back() : window.location = "https://metaverse.highfidelity.com/marketplace?";
         });
         $("#all-markets").on("click", function () {
             EventBridge.emitWebEvent(GOTO_DIRECTORY);
diff --git a/scripts/system/html/marketplaces.html b/scripts/system/html/marketplaces.html
index 6051a9df96..9e001c064f 100644
--- a/scripts/system/html/marketplaces.html
+++ b/scripts/system/html/marketplaces.html
@@ -25,25 +25,25 @@
                 </div>
                 <div class="marketplace-tile-second-column">
                     <p class="marketplace-tile-description">This is the default High Fidelity marketplace.  Viewing and downloading content from here is fully supported in Interface.</p>
-                    <div class="exploreButton-holder">
-                        <input class="blue exploreButton" type="button" value="Explore" id="exploreHifiMarketplace" />
-                    </div>
-                    <hr class="tile-divider">
                 </div>
-                <div class="marketplace-tile">
-                    <div class="marketplace-tile-first-column">
-                        <img class="marketplace-tile-image" src="img/clara-tile.png">
-                    </div>
-                    <div class="marketplace-tile-second-column">
-                        <p class="marketplace-tile-description">Clara.io has thousands of models available for importing into High Fidelity.  Follow these steps for the best experience:</p>
-                        <ol class="marketplace-clara-steps">
-                            <li><a id="claraSignUp" href="http://www.clara.io/signup">Create an account here </a>or log in as an existing user.</li>
-                            <li>Choose a model from the list and click &ldquo;Download to High Fidelity&rdquo;.</li>
-                        </ol>
-                        <div class="exploreButton-holder">
-                            <input class="blue exploreButton" type="button" value="Explore" id="exploreClaraMarketplace" />
-                        </div>
-                    </div>
+                <div class="exploreButton-holder">
+                    <input class="blue exploreButton" type="button" value="Explore" id="exploreHifiMarketplace" />
+                </div>
+                <hr class="tile-divider">
+            </div>
+            <div class="marketplace-tile">
+                <div class="marketplace-tile-first-column">
+                    <img class="marketplace-tile-image" src="img/clara-tile.png">
+                </div>
+                <div class="marketplace-tile-second-column">
+                    <p class="marketplace-tile-description">Clara.io has thousands of models available for importing into High Fidelity.  Follow these steps for the best experience:</p>
+                    <ol class="marketplace-clara-steps">
+                        <li><a id="claraSignUp" href="http://www.clara.io/signup">Create an account here </a>or log in as an existing user.</li>
+                        <li>Choose a model from the list and click &ldquo;Download to High Fidelity&rdquo;.</li>
+                    </ol>
+                </div>
+                <div class="exploreButton-holder">
+                    <input class="blue exploreButton" type="button" value="Explore" id="exploreClaraMarketplace" />
                 </div>
             </div>
         </div>
diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js
index 53f88ea62d..c3d55d5875 100644
--- a/scripts/system/libraries/WebTablet.js
+++ b/scripts/system/libraries/WebTablet.js
@@ -42,7 +42,7 @@ var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-
 // returns object with two fields:
 //    * position - position in front of the user
 //    * rotation - rotation of entity so it faces the user.
-function calcSpawnInfo(hand, tabletHeight) {
+function calcSpawnInfo(hand, tabletHeight, landscape) {
     var finalPosition;
 
     var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position;
@@ -81,7 +81,7 @@ function calcSpawnInfo(hand, tabletHeight) {
 
         return {
             position: position,
-            rotation: rotation
+            rotation: landscape ? Quat.multiply(rotation, { x: 0.0, y: 0.0, z: 0.707, w: 0.707 }) : rotation
         };
     } else {
         var forward = Quat.getForward(headRot);
@@ -89,7 +89,7 @@ function calcSpawnInfo(hand, tabletHeight) {
         var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, {x: 0, y: 1, z: 0});
         return {
             position: finalPosition,
-            rotation: Quat.multiply(orientation, {x: 0, y: 1, z: 0, w: 0})
+            rotation: landscape ? Quat.multiply(orientation, ROT_LANDSCAPE) : Quat.multiply(orientation, ROT_Y_180)
         };
     }
 }
@@ -424,7 +424,7 @@ WebTablet.prototype.calculateTabletAttachmentProperties = function (hand, useMou
         tabletProperties.parentJointIndex = SENSOR_TO_ROOM_MATRIX;
 
         // compute the appropriate position of the tablet, near the hand controller that was used to spawn it.
-        var spawnInfo = calcSpawnInfo(hand, this.height);
+        var spawnInfo = calcSpawnInfo(hand, this.height, this.landscape);
         tabletProperties.position = spawnInfo.position;
         tabletProperties.rotation = spawnInfo.rotation;
     } else {
diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp
index ce47a896aa..dbb315a9ae 100644
--- a/tests/render-perf/src/main.cpp
+++ b/tests/render-perf/src/main.cpp
@@ -681,7 +681,7 @@ private:
         _renderCount = _renderThread._presentCount.load();
         update();
 
-        RenderArgs renderArgs(_renderThread._gpuContext, _octree, DEFAULT_OCTREE_SIZE_SCALE,
+        RenderArgs renderArgs(_renderThread._gpuContext, DEFAULT_OCTREE_SIZE_SCALE,
             0, RenderArgs::DEFAULT_RENDER_MODE,
             RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE);
 
diff --git a/tests/render-texture-load/src/GLIHelpers.h b/tests/render-texture-load/src/GLIHelpers.h
index c2841311a9..886176e874 100644
--- a/tests/render-texture-load/src/GLIHelpers.h
+++ b/tests/render-texture-load/src/GLIHelpers.h
@@ -28,6 +28,7 @@
 #pragma GCC diagnostic ignored "-Wunused-but-set-variable"
 #pragma GCC diagnostic ignored "-Wunused-result"
 #pragma GCC diagnostic ignored "-Wignored-qualifiers"
+#pragma GCC diagnostic ignored "-Wtype-limits"
 #endif
 
 #include <gli/gli.hpp>