diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp
index dd25aa4c4b..49b4b1ced4 100644
--- a/assignment-client/src/avatars/AvatarMixerSlave.cpp
+++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp
@@ -168,7 +168,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
         QList<AvatarSharedPointer> avatarList;
         std::unordered_map<AvatarSharedPointer, SharedNodePointer> avatarDataToNodes;
 
-        int listItem = 0;
         std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) {
             const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
 
@@ -176,7 +175,6 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
             // but not have yet sent data that's linked to the node. Check for that case and don't
             // consider those nodes.
             if (otherNodeData) {
-                listItem++;
                 AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer();
                 avatarList << otherAvatar;
                 avatarDataToNodes[otherAvatar] = otherNode;
@@ -185,8 +183,8 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
 
         AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer();
         ViewFrustum cameraView = nodeData->getViewFrustom();
-        std::priority_queue<AvatarPriority> sortedAvatars = AvatarData::sortAvatars(
-                avatarList, cameraView,
+        std::priority_queue<AvatarPriority> sortedAvatars;
+        AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
 
                 [&](AvatarSharedPointer avatar)->uint64_t{
                     auto avatarNode = avatarDataToNodes[avatar];
diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp
index f5fe82ef4b..6e1f44f5ac 100644
--- a/interface/src/avatar/Avatar.cpp
+++ b/interface/src/avatar/Avatar.cpp
@@ -351,7 +351,6 @@ void Avatar::simulate(float deltaTime, bool inView) {
             _jointDataSimulationRate.increment();
 
             _skeletonModel->simulate(deltaTime, true);
-            _skeletonModelSimulationRate.increment();
 
             locationChanged(); // joints changed, so if there are any children, update them.
             _hasNewJointData = false;
@@ -367,8 +366,8 @@ void Avatar::simulate(float deltaTime, bool inView) {
         } else {
             // a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
             _skeletonModel->simulate(deltaTime, false);
-            _skeletonModelSimulationRate.increment();
         }
+        _skeletonModelSimulationRate.increment();
     }
 
     // update animation for display name fade in/out
diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp
index d806c042b9..7417f73102 100644
--- a/interface/src/avatar/AvatarManager.cpp
+++ b/interface/src/avatar/AvatarManager.cpp
@@ -157,15 +157,14 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
     lock.unlock();
 
     PerformanceTimer perfTimer("otherAvatars");
-    uint64_t startTime = usecTimestampNow();
 
     auto avatarMap = getHashCopy();
     QList<AvatarSharedPointer> avatarList = avatarMap.values();
     ViewFrustum cameraView;
     qApp->copyDisplayViewFrustum(cameraView);
 
-    std::priority_queue<AvatarPriority> sortedAvatars = AvatarData::sortAvatars(
-        avatarList, cameraView,
+    std::priority_queue<AvatarPriority> sortedAvatars;
+    AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
 
         [](AvatarSharedPointer avatar)->uint64_t{
             return std::static_pointer_cast<Avatar>(avatar)->getLastRenderUpdateTime();
@@ -194,10 +193,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
         });
 
     render::PendingChanges pendingChanges;
-    const uint64_t RENDER_UPDATE_BUDGET = 1500; // usec
-    const uint64_t MAX_UPDATE_BUDGET = 2000; // usec
-    uint64_t renderExpiry = startTime + RENDER_UPDATE_BUDGET;
-    uint64_t maxExpiry = startTime + MAX_UPDATE_BUDGET;
+    uint64_t startTime = usecTimestampNow();
+    const uint64_t UPDATE_BUDGET = 2000; // usec
+    uint64_t updateExpiry = startTime + UPDATE_BUDGET;
 
     int numAvatarsUpdated = 0;
     int numAVatarsNotUpdated = 0;
@@ -223,7 +221,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
 
         const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY;
         uint64_t now = usecTimestampNow();
-        if (now < renderExpiry) {
+        if (now < updateExpiry) {
             // we're within budget
             bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
             if (inView && avatar->hasNewJointData()) {
@@ -232,21 +230,13 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
             avatar->simulate(deltaTime, inView);
             avatar->updateRenderItem(pendingChanges);
             avatar->setLastRenderUpdateTime(startTime);
-        } else if (now < maxExpiry) {
-            // we've spent most of our time budget, but we still simulate() the avatar as it if were out of view
-            // --> some avatars may freeze until their priority trickles up
-            bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
-            if (inView && avatar->hasNewJointData()) {
-                numAVatarsNotUpdated++;
-            }
-            avatar->simulate(deltaTime, false);
         } else {
-            // we've spent ALL of our time budget --> bail on the rest of the avatar updates
+            // we've spent our full time budget --> bail on the rest of the avatar updates
             // --> more avatars may freeze until their priority trickles up
             // --> some scale or fade animations may glitch
             // --> some avatar velocity measurements may be a little off
 
-            // HACK: no time simulate, but we will take the time to count how many were tragically missed
+            // no time simulate, but we take the time to count how many were tragically missed
             bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
             if (!inView) {
                 break;
diff --git a/interface/src/avatar/CauterizedModel.cpp b/interface/src/avatar/CauterizedModel.cpp
index 843779dd3b..0c3d863649 100644
--- a/interface/src/avatar/CauterizedModel.cpp
+++ b/interface/src/avatar/CauterizedModel.cpp
@@ -95,12 +95,6 @@ void CauterizedModel::createCollisionRenderItemSet() {
 	Model::createCollisionRenderItemSet();
 }
 
-// Called within Model::simulate call, below.
-void CauterizedModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
-    Model::updateRig(deltaTime, parentTransform);
-    _needsUpdateClusterMatrices = true;
-}
-
 void CauterizedModel::updateClusterMatrices() {
     PerformanceTimer perfTimer("CauterizedModel::updateClusterMatrices");
 
diff --git a/interface/src/avatar/CauterizedModel.h b/interface/src/avatar/CauterizedModel.h
index 01e0b13650..ba12aee32b 100644
--- a/interface/src/avatar/CauterizedModel.h
+++ b/interface/src/avatar/CauterizedModel.h
@@ -37,7 +37,6 @@ public:
 	void createVisibleRenderItemSet() override;
     void createCollisionRenderItemSet() override;
 
-    virtual void updateRig(float deltaTime, glm::mat4 parentTransform) override;
     virtual void updateClusterMatrices() override;
     void updateRenderItems() override;
 
diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp
index 4b77323bba..476abf8d4b 100644
--- a/interface/src/avatar/SkeletonModel.cpp
+++ b/interface/src/avatar/SkeletonModel.cpp
@@ -166,7 +166,7 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
         _rig->computeMotionAnimationState(deltaTime, position, velocity, orientation, ccState);
 
         // evaluate AnimGraph animation and update jointStates.
-        CauterizedModel::updateRig(deltaTime, parentTransform);
+        Model::updateRig(deltaTime, parentTransform);
 
         Rig::EyeParameters eyeParams;
         eyeParams.worldHeadOrientation = headParams.worldHeadOrientation;
@@ -179,7 +179,9 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
 
         _rig->updateFromEyeParameters(eyeParams);
     } else {
-        CauterizedModel::updateRig(deltaTime, parentTransform);
+        // no need to call Model::updateRig() because otherAvatars get their joint state
+        // copied directly from AvtarData::_jointData (there are no Rig animations to blend)
+        _needsUpdateClusterMatrices = true;
 
         // This is a little more work than we really want.
         //
diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp
index c47da7c0b0..84e34adec7 100644
--- a/libraries/animation/src/Rig.cpp
+++ b/libraries/animation/src/Rig.cpp
@@ -1307,39 +1307,50 @@ void Rig::copyJointsIntoJointData(QVector<JointData>& jointDataVec) const {
 void Rig::copyJointsFromJointData(const QVector<JointData>& jointDataVec) {
     PerformanceTimer perfTimer("copyJoints");
     PROFILE_RANGE(simulation_animation_detail, "copyJoints");
-    if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._relativePoses.size()) {
-        // make a vector of rotations in absolute-geometry-frame
-        const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses();
-        std::vector<glm::quat> rotations;
-        rotations.reserve(absoluteDefaultPoses.size());
-        const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform));
-        for (int i = 0; i < jointDataVec.size(); i++) {
-            const JointData& data = jointDataVec.at(i);
-            if (data.rotationSet) {
-                // JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame
-                rotations.push_back(rigToGeometryRot * data.rotation);
-            } else {
-                rotations.push_back(absoluteDefaultPoses[i].rot());
-            }
-        }
+    if (!_animSkeleton) {
+        return;
+    }
+    if (jointDataVec.size() != (int)_internalPoseSet._relativePoses.size()) {
+        // animations haven't fully loaded yet.
+        _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();
+    }
 
-        // convert rotations from absolute to parent relative.
-        _animSkeleton->convertAbsoluteRotationsToRelative(rotations);
-
-        // store new relative poses
-        const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses();
-        for (int i = 0; i < jointDataVec.size(); i++) {
-            const JointData& data = jointDataVec.at(i);
-            _internalPoseSet._relativePoses[i].scale() = Vectors::ONE;
-            _internalPoseSet._relativePoses[i].rot() = rotations[i];
-            if (data.translationSet) {
-                // JointData translations are in scaled relative-frame so we scale back to regular relative-frame
-                _internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation;
-            } else {
-                _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans();
-            }
+    // make a vector of rotations in absolute-geometry-frame
+    const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses();
+    std::vector<glm::quat> rotations;
+    rotations.reserve(absoluteDefaultPoses.size());
+    const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform));
+    for (int i = 0; i < jointDataVec.size(); i++) {
+        const JointData& data = jointDataVec.at(i);
+        if (data.rotationSet) {
+            // JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame
+            rotations.push_back(rigToGeometryRot * data.rotation);
+        } else {
+            rotations.push_back(absoluteDefaultPoses[i].rot());
         }
     }
+
+    // convert rotations from absolute to parent relative.
+    _animSkeleton->convertAbsoluteRotationsToRelative(rotations);
+
+    // store new relative poses
+    const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses();
+    for (int i = 0; i < jointDataVec.size(); i++) {
+        const JointData& data = jointDataVec.at(i);
+        _internalPoseSet._relativePoses[i].scale() = Vectors::ONE;
+        _internalPoseSet._relativePoses[i].rot() = rotations[i];
+        if (data.translationSet) {
+            // JointData translations are in scaled relative-frame so we scale back to regular relative-frame
+            _internalPoseSet._relativePoses[i].trans() = _invGeometryOffset.scale() * data.translation;
+        } else {
+            _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans();
+        }
+    }
+
+    // build absolute poses and copy to externalPoseSet
+    buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses);
+    QWriteLocker writeLock(&_externalPoseSetLock);
+    _externalPoseSet = _internalPoseSet;
 }
 
 void Rig::computeAvatarBoundingCapsule(
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index c1dd60a3b0..8025c680ca 100644
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -2324,61 +2324,57 @@ float AvatarData::_avatarSortCoefficientSize { 0.5f };
 float AvatarData::_avatarSortCoefficientCenter { 0.25 };
 float AvatarData::_avatarSortCoefficientAge { 1.0f };
 
-std::priority_queue<AvatarPriority> AvatarData::sortAvatars(
-    QList<AvatarSharedPointer> avatarList,
-    const ViewFrustum& cameraView,
-    std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
-    std::function<float(AvatarSharedPointer)> getBoundingRadius,
-    std::function<bool(AvatarSharedPointer)> shouldIgnore) {
+void AvatarData::sortAvatars(
+        QList<AvatarSharedPointer> avatarList,
+        const ViewFrustum& cameraView,
+        std::priority_queue<AvatarPriority>& sortedAvatarsOut,
+        std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
+        std::function<float(AvatarSharedPointer)> getBoundingRadius,
+        std::function<bool(AvatarSharedPointer)> shouldIgnore) {
 
-    uint64_t startTime = usecTimestampNow();
+    PROFILE_RANGE(simulation, "sort");
+    uint64_t now = usecTimestampNow();
 
     glm::vec3 frustumCenter = cameraView.getPosition();
+    const glm::vec3& forward = cameraView.getDirection();
+    for (int32_t i = 0; i < avatarList.size(); ++i) {
+        const auto& avatar = avatarList.at(i);
 
-    std::priority_queue<AvatarPriority> sortedAvatars;
-    {
-        PROFILE_RANGE(simulation, "sort");
-        for (int32_t i = 0; i < avatarList.size(); ++i) {
-            const auto& avatar = avatarList.at(i);
-
-            if (shouldIgnore(avatar)) {
-                continue;
-            }
-
-            // priority = weighted linear combination of:
-            //   (a) apparentSize
-            //   (b) proximity to center of view
-            //   (c) time since last update
-            glm::vec3 avatarPosition = avatar->getPosition();
-            glm::vec3 offset = avatarPosition - frustumCenter;
-            float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
-
-            // FIXME - AvatarData has something equivolent to this
-            float radius = getBoundingRadius(avatar);
-
-            const glm::vec3& forward = cameraView.getDirection();
-            float apparentSize = 2.0f * radius / distance;
-            float cosineAngle = glm::length(glm::dot(offset, forward) * forward) / distance;
-            float age = (float)(startTime - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND);
-
-            // NOTE: we are adding values of different units to get a single measure of "priority".
-            // Thus we multiply each component by a conversion "weight" that scales its units relative to the others.
-            // These weights are pure magic tuning and should be hard coded in the relation below,
-            // but are currently exposed for anyone who would like to explore fine tuning:
-            float priority = _avatarSortCoefficientSize * apparentSize
-                + _avatarSortCoefficientCenter * cosineAngle
-                + _avatarSortCoefficientAge * age;
-
-            // decrement priority of avatars outside keyhole
-            if (distance > cameraView.getCenterRadius()) {
-                if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) {
-                    priority += OUT_OF_VIEW_PENALTY;
-                }
-            }
-            sortedAvatars.push(AvatarPriority(avatar, priority));
+        if (shouldIgnore(avatar)) {
+            continue;
         }
+
+        // priority = weighted linear combination of:
+        //   (a) apparentSize
+        //   (b) proximity to center of view
+        //   (c) time since last update
+        glm::vec3 avatarPosition = avatar->getPosition();
+        glm::vec3 offset = avatarPosition - frustumCenter;
+        float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
+
+        // FIXME - AvatarData has something equivolent to this
+        float radius = getBoundingRadius(avatar);
+
+        float apparentSize = 2.0f * radius / distance;
+        float cosineAngle = glm::dot(offset, forward) / distance;
+        float age = (float)(now - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND);
+
+        // NOTE: we are adding values of different units to get a single measure of "priority".
+        // Thus we multiply each component by a conversion "weight" that scales its units relative to the others.
+        // These weights are pure magic tuning and should be hard coded in the relation below,
+        // but are currently exposed for anyone who would like to explore fine tuning:
+        float priority = _avatarSortCoefficientSize * apparentSize
+            + _avatarSortCoefficientCenter * cosineAngle
+            + _avatarSortCoefficientAge * age;
+
+        // decrement priority of avatars outside keyhole
+        if (distance > cameraView.getCenterRadius()) {
+            if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) {
+                priority += OUT_OF_VIEW_PENALTY;
+            }
+        }
+        sortedAvatarsOut.push(AvatarPriority(avatar, priority));
     }
-    return sortedAvatars;
 }
 
 QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) {
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index 12209d9c31..c2240f400f 100644
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -597,9 +597,10 @@ public:
 
     static const float OUT_OF_VIEW_PENALTY;
 
-    static std::priority_queue<AvatarPriority> sortAvatars(
+    static void sortAvatars(
         QList<AvatarSharedPointer> avatarList,
         const ViewFrustum& cameraView,
+        std::priority_queue<AvatarPriority>& sortedAvatarsOut,
         std::function<uint64_t(AvatarSharedPointer)> getLastUpdated,
         std::function<float(AvatarSharedPointer)> getBoundingRadius,
         std::function<bool(AvatarSharedPointer)> shouldIgnore);