diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 801f28c6f5..6522841dc5 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,8 @@ #include #include #include +#include "../AssignmentDynamicFactory.h" +#include "../entities/AssignmentParentFinder.h" const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; @@ -55,6 +58,9 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : ThreadedAssignment(message), _slavePool(&_slaveSharedData) { + DependencyManager::registerInheritance(); + DependencyManager::set(); + // make sure we hear about node kills so we can tell the other nodes connect(DependencyManager::get().data(), &NodeList::nodeKilled, this, &AvatarMixer::handleAvatarKilled); @@ -69,6 +75,8 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket"); packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket"); + packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, + this, "handleOctreePacket"); packetReceiver.registerListenerForTypes({ PacketType::ReplicatedAvatarIdentity, @@ -240,6 +248,10 @@ void AvatarMixer::start() { int lockWait, nodeTransform, functor; + { + _entityViewer.queryOctree(); + } + // Allow nodes to process any pending/queued packets across our worker threads { auto start = usecTimestampNow(); @@ -775,6 +787,7 @@ void AvatarMixer::sendStatsPacket() { slavesAggregatObject["sent_4_averageDataBytes"] = TIGHT_LOOP_STAT(aggregateStats.numDataBytesSent); slavesAggregatObject["sent_5_averageTraitsBytes"] = TIGHT_LOOP_STAT(aggregateStats.numTraitsBytesSent); slavesAggregatObject["sent_6_averageIdentityBytes"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityBytesSent); + slavesAggregatObject["sent_7_averageHeroAvatars"] = TIGHT_LOOP_STAT(aggregateStats.numHeroesIncluded); slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime); slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime); @@ -882,13 +895,15 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node void AvatarMixer::domainSettingsRequestComplete() { auto nodeList = DependencyManager::get(); nodeList->addSetOfNodeTypesToNodeInterestSet({ - NodeType::Agent, NodeType::EntityScriptServer, + NodeType::Agent, NodeType::EntityScriptServer, NodeType::EntityServer, NodeType::UpstreamAvatarMixer, NodeType::DownstreamAvatarMixer }); // parse the settings to pull out the values we need parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject()); + setupEntityQuery(); + // start our tight loop... start(); } @@ -976,3 +991,45 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { qCDebug(avatars) << "Avatars other than" << _slaveSharedData.skeletonURLWhitelist << "will be replaced by" << (_slaveSharedData.skeletonReplacementURL.isEmpty() ? "default" : _slaveSharedData.skeletonReplacementURL.toString()); } } + +void AvatarMixer::setupEntityQuery() { + _entityViewer.init(); + DependencyManager::registerInheritance(); + DependencyManager::set(_entityViewer.getTree()); + _slaveSharedData.entityTree = _entityViewer.getTree(); + + // ES query: {"avatarPriority": true, "type": "Zone"} + QJsonObject priorityZoneQuery; + priorityZoneQuery["avatarPriority"] = true; + priorityZoneQuery["type"] = "Zone"; + + _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery); +} + +void AvatarMixer::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { + PacketType packetType = message->getType(); + + switch (packetType) { + case PacketType::OctreeStats: + break; + + case PacketType::EntityData: + _entityViewer.processDatagram(*message, senderNode); + break; + + case PacketType::EntityErase: + _entityViewer.processEraseMessage(*message, senderNode); + break; + + default: + qCDebug(avatars) << "Unexpected packet type:" << packetType; + break; + } +} + +void AvatarMixer::aboutToFinish() { + DependencyManager::destroy(); + DependencyManager::destroy(); + + ThreadedAssignment::aboutToFinish(); +} diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 2992e19b8f..9393ea6c56 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -20,6 +20,7 @@ #include #include +#include "../entities/EntityTreeHeadlessViewer.h" #include "AvatarMixerClientData.h" #include "AvatarMixerSlavePool.h" @@ -29,6 +30,7 @@ class AvatarMixer : public ThreadedAssignment { Q_OBJECT public: AvatarMixer(ReceivedMessage& message); + virtual void aboutToFinish() override; static bool shouldReplicateTo(const Node& from, const Node& to) { return to.getType() == NodeType::DownstreamAvatarMixer && @@ -57,6 +59,7 @@ private slots: void handleReplicatedBulkAvatarPacket(QSharedPointer message); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); + void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode); void start(); private: @@ -71,8 +74,13 @@ private: void optionallyReplicatePacket(ReceivedMessage& message, const Node& node); + void setupEntityQuery(); + p_high_resolution_clock::time_point _lastFrameTimestamp; + // Attach to entity tree for avatar-priority zone info. + EntityTreeHeadlessViewer _entityViewer; + // FIXME - new throttling - use these values somehow float _trailingMixRatio { 0.0f }; float _throttlingRatio { 0.0f }; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index b7d2f5cdf8..e24d48a9ed 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -16,6 +16,10 @@ #include #include +#include +#include + +#include "AvatarLogging.h" #include "AvatarMixerSlave.h" @@ -62,7 +66,7 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData switch (packet->getType()) { case PacketType::AvatarData: - parseData(*packet); + parseData(*packet, slaveSharedData); break; case PacketType::SetAvatarTraits: processSetTraitsMessage(*packet, slaveSharedData, *node); @@ -80,7 +84,38 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData return packetsProcessed; } -int AvatarMixerClientData::parseData(ReceivedMessage& message) { +namespace { + using std::static_pointer_cast; + + // Operator to find if a point is within an avatar-priority (hero) Zone Entity. + struct FindPriorityZone { + glm::vec3 position; + bool isInPriorityZone { false }; + static bool operation(const OctreeElementPointer& element, void* extraData) { + auto findPriorityZone = static_cast(extraData); + if (element->getAACube().contains(findPriorityZone->position)) { + const EntityTreeElementPointer entityTreeElement = static_pointer_cast(element); + entityTreeElement->forEachEntity([&findPriorityZone](EntityItemPointer item) { + if (item->getType() == EntityTypes::Zone + && item->contains(findPriorityZone->position) + && static_pointer_cast(item)->getAvatarPriority()) { + findPriorityZone->isInPriorityZone = true; + } + }); + if (findPriorityZone->isInPriorityZone) { + return false; // For now, stop at first hit. + } else { + return true; + } + } else { // Position isn't within this subspace, so end recursion. + return false; + } + } + }; + +} // Close anonymous namespace. + +int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveSharedData& slaveSharedData) { // pull the sequence number from the data first uint16_t sequenceNumber; @@ -90,9 +125,33 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) { incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; + glm::vec3 oldPosition = getPosition(); // compute the offset to the data payload - return _avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead())); + if (!_avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()))) { + return false; + } + + auto newPosition = getPosition(); + if (newPosition != oldPosition) { +//#define AVATAR_HERO_TEST_HACK +#ifdef AVATAR_HERO_TEST_HACK + { + const static QString heroKey { "HERO" }; + _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey)); + } +#else + EntityTree& entityTree = *slaveSharedData.entityTree; + FindPriorityZone findPriorityZone { newPosition, false } ; + entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); + _avatar->setPriorityAvatar(findPriorityZone.isInPriorityZone); + //if (findPriorityZone.isInPriorityZone) { + // qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; + //} +#endif + } + + return true; } void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 843f19cf22..98c8d7e15b 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -21,7 +21,7 @@ #include #include -#include +#include "MixerAvatar.h" #include #include #include @@ -45,11 +45,12 @@ public: using HRCTime = p_high_resolution_clock::time_point; using PerNodeTraitVersions = std::unordered_map; - int parseData(ReceivedMessage& message) override; - AvatarData& getAvatar() { return *_avatar; } - const AvatarData& getAvatar() const { return *_avatar; } - const AvatarData* getConstAvatarData() const { return _avatar.get(); } - AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } + using NodeData::parseData; // Avoid clang warning about hiding. + int parseData(ReceivedMessage& message, const SlaveSharedData& SlaveSharedData); + MixerAvatar& getAvatar() { return *_avatar; } + const MixerAvatar& getAvatar() const { return *_avatar; } + const MixerAvatar* getConstAvatarData() const { return _avatar.get(); } + MixerAvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } uint16_t getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const; void setLastBroadcastSequenceNumber(NLPacket::LocalID nodeID, uint16_t sequenceNumber) @@ -163,7 +164,7 @@ private: }; PacketQueue _packetQueue; - AvatarSharedPointer _avatar { new AvatarData() }; + MixerAvatarSharedPointer _avatar { new MixerAvatar() }; uint16_t _lastReceivedSequenceNumber { 0 }; std::unordered_map _lastBroadcastSequenceNumbers; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 6b039e2c03..a38553ddeb 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -281,7 +281,34 @@ AABox computeBubbleBox(const AvatarData& avatar, float bubbleExpansionFactor) { return box; } +namespace { + class SortableAvatar : public PrioritySortUtil::Sortable { + public: + SortableAvatar() = delete; + SortableAvatar(const MixerAvatar* avatar, const Node* avatarNode, uint64_t lastEncodeTime) + : _avatar(avatar), _node(avatarNode), _lastEncodeTime(lastEncodeTime) { + } + glm::vec3 getPosition() const override { return _avatar->getClientGlobalPosition(); } + float getRadius() const override { + glm::vec3 nodeBoxScale = _avatar->getGlobalBoundingBox().getScale(); + return 0.5f * glm::max(nodeBoxScale.x, glm::max(nodeBoxScale.y, nodeBoxScale.z)); + } + uint64_t getTimestamp() const override { + return _lastEncodeTime; + } + const Node* getNode() const { return _node; } + const MixerAvatar* getAvatar() const { return _avatar; } + + private: + const MixerAvatar* _avatar; + const Node* _node; + uint64_t _lastEncodeTime; + }; + +} // Close anonymous namespace. + void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) { + const float AVATAR_HERO_FRACTION { 0.4f }; const Node* destinationNode = node.data(); auto nodeList = DependencyManager::get(); @@ -314,8 +341,9 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) int identityBytesSent = 0; int traitBytesSent = 0; - // max number of avatarBytes per frame - int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); + // max number of avatarBytes per frame (13 900, typical) + const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); + const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; @@ -339,34 +367,21 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const float MY_AVATAR_BUBBLE_EXPANSION_FACTOR = 4.0f; // magic number determined emperically AABox nodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR); - class SortableAvatar: public PrioritySortUtil::Sortable { - public: - SortableAvatar() = delete; - SortableAvatar(const AvatarData* avatar, const Node* avatarNode, uint64_t lastEncodeTime) - : _avatar(avatar), _node(avatarNode), _lastEncodeTime(lastEncodeTime) {} - glm::vec3 getPosition() const override { return _avatar->getClientGlobalPosition(); } - float getRadius() const override { - glm::vec3 nodeBoxScale = _avatar->getGlobalBoundingBox().getScale(); - return 0.5f * glm::max(nodeBoxScale.x, glm::max(nodeBoxScale.y, nodeBoxScale.z)); - } - uint64_t getTimestamp() const override { - return _lastEncodeTime; - } - const Node* getNode() const { return _node; } - - private: - const AvatarData* _avatar; - const Node* _node; - uint64_t _lastEncodeTime; - }; - // prepare to sort const auto& cameraViews = nodeData->getViewFrustums(); - PrioritySortUtil::PriorityQueue sortedAvatars(cameraViews, - AvatarData::_avatarSortCoefficientSize, - AvatarData::_avatarSortCoefficientCenter, - AvatarData::_avatarSortCoefficientAge); - sortedAvatars.reserve(_end - _begin); + + using AvatarPriorityQueue = PrioritySortUtil::PriorityQueue; + // Keep two independent queues, one for heroes and one for the riff-raff. + enum PriorityVariants { kHero, kNonhero }; + AvatarPriorityQueue avatarPriorityQueues[2] = + { + {cameraViews, AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, AvatarData::_avatarSortCoefficientAge}, + {cameraViews, AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, AvatarData::_avatarSortCoefficientAge} + }; + + avatarPriorityQueues[kNonhero].reserve(_end - _begin); for (auto listedNode = _begin; listedNode != _end; ++listedNode) { Node* otherNodeRaw = (*listedNode).data(); @@ -446,10 +461,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) if (!shouldIgnore) { // sort this one for later - const AvatarData* avatarNodeData = avatarClientNodeData->getConstAvatarData(); + const MixerAvatar* avatarNodeData = avatarClientNodeData->getConstAvatarData(); auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNode->getLocalID()); - sortedAvatars.push(SortableAvatar(avatarNodeData, avatarNode, lastEncodeTime)); + avatarPriorityQueues[avatarNodeData->getPriorityAvatar() ? kHero : kNonhero].push( + SortableAvatar(avatarNodeData, avatarNode, lastEncodeTime)); } // If Avatar A's PAL WAS open but is no longer open, AND @@ -475,115 +491,132 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // loop through our sorted avatars and allocate our bandwidth to them accordingly - int remainingAvatars = (int)sortedAvatars.size(); + int remainingAvatars = (int)avatarPriorityQueues[kHero].size() + (int)avatarPriorityQueues[kNonhero].size(); auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true); auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData); const int avatarPacketCapacity = avatarPacket->getPayloadCapacity(); int avatarSpaceAvailable = avatarPacketCapacity; int numPacketsSent = 0; + int numAvatarsSent = 0; auto identityPacketList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); - const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst); - for (const auto& sortedAvatar : sortedAvatarVector) { - const Node* otherNode = sortedAvatar.getNode(); - auto lastEncodeForOther = sortedAvatar.getTimestamp(); + for (PriorityVariants currentVariant = kHero; currentVariant <= kNonhero; ++((int&)currentVariant)) { + const auto& sortedAvatarVector = avatarPriorityQueues[currentVariant].getSortedVector(numToSendEst); + for (const auto& sortedAvatar : sortedAvatarVector) { + const Node* otherNode = sortedAvatar.getNode(); + auto lastEncodeForOther = sortedAvatar.getTimestamp(); - assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map + assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map - AvatarData::AvatarDataDetail detail = AvatarData::NoData; + AvatarData::AvatarDataDetail detail = AvatarData::NoData; - // NOTE: Here's where we determine if we are over budget and drop remaining avatars, - // or send minimal avatar data in uncommon case of PALIsOpen. - int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars; - auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes; - bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame; - if (overBudget) { - if (PALIsOpen) { - _stats.overBudgetAvatars++; - detail = AvatarData::PALMinimum; - } else { - _stats.overBudgetAvatars += remainingAvatars; - break; + // NOTE: Here's where we determine if we are over budget and drop remaining avatars, + // or send minimal avatar data in uncommon case of PALIsOpen. + int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars; + auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes; + bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame; + if (overBudget) { + if (PALIsOpen) { + _stats.overBudgetAvatars++; + detail = AvatarData::PALMinimum; + } else { + _stats.overBudgetAvatars += remainingAvatars; + break; + } + } + + bool overHeroBudget = currentVariant == kHero && numAvatarDataBytes > maxHeroBytesPerFrame; + if (overHeroBudget) { + break; // No more heroes (this frame). + } + + auto startAvatarDataPacking = chrono::high_resolution_clock::now(); + + const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); + const MixerAvatar* otherAvatar = otherNodeData->getConstAvatarData(); + + // Typically all out-of-view avatars but such avatars' priorities will rise with time: + bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; + + if (isLowerPriority) { + detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData; + nodeData->incrementAvatarOutOfView(); + } else if (!overBudget) { + detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; + nodeData->incrementAvatarInView(); + + // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO + // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. + if (otherAvatar->hasProcessedFirstIdentity() + && nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) { + identityBytesSent += sendIdentityPacket(*identityPacketList, otherNodeData, *destinationNode); + + // remember the last time we sent identity details about this other node to the receiver + nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow()); + } + } + + QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getLocalID()); + + const bool distanceAdjust = true; + const bool dropFaceTracking = false; + AvatarDataPacket::SendStatus sendStatus; + sendStatus.sendUUID = true; + + do { + auto startSerialize = chrono::high_resolution_clock::now(); + QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, + sendStatus, dropFaceTracking, distanceAdjust, myPosition, + &lastSentJointsForOther, avatarSpaceAvailable); + auto endSerialize = chrono::high_resolution_clock::now(); + _stats.toByteArrayElapsedTime += + (quint64)chrono::duration_cast(endSerialize - startSerialize).count(); + + avatarPacket->write(bytes); + avatarSpaceAvailable -= bytes.size(); + numAvatarDataBytes += bytes.size(); + if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { + // Weren't able to fit everything. + nodeList->sendPacket(std::move(avatarPacket), *destinationNode); + ++numPacketsSent; + avatarPacket = NLPacket::create(PacketType::BulkAvatarData); + avatarSpaceAvailable = avatarPacketCapacity; + } + } while (!sendStatus); + + if (detail != AvatarData::NoData) { + _stats.numOthersIncluded++; + if (otherAvatar->getPriorityAvatar()) { + _stats.numHeroesIncluded++; + } + + // increment the number of avatars sent to this receiver + nodeData->incrementNumAvatarsSentLastFrame(); + + // set the last sent sequence number for this sender on the receiver + nodeData->setLastBroadcastSequenceNumber(otherNode->getLocalID(), + otherNodeData->getLastReceivedSequenceNumber()); + nodeData->setLastOtherAvatarEncodeTime(otherNode->getLocalID(), usecTimestampNow()); + } + + auto endAvatarDataPacking = chrono::high_resolution_clock::now(); + _stats.avatarDataPackingElapsedTime += + (quint64)chrono::duration_cast(endAvatarDataPacking - startAvatarDataPacking).count(); + + if (!overBudget) { + // use helper to add any changed traits to our packet list + traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList); + } + numAvatarsSent++; + remainingAvatars--; + } + + if (currentVariant == kHero) { // Dump any remaining heroes into the commoners. + for (auto avIter = sortedAvatarVector.begin() + numAvatarsSent; avIter < sortedAvatarVector.end(); ++avIter) { + avatarPriorityQueues[kNonhero].push(*avIter); } } - - auto startAvatarDataPacking = chrono::high_resolution_clock::now(); - - const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); - const AvatarData* otherAvatar = otherNodeData->getConstAvatarData(); - - // Typically all out-of-view avatars but such avatars' priorities will rise with time: - bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; - - if (isLowerPriority) { - detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData; - nodeData->incrementAvatarOutOfView(); - } else if (!overBudget) { - detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; - nodeData->incrementAvatarInView(); - - // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO - // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. - if (otherAvatar->hasProcessedFirstIdentity() - && nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) { - identityBytesSent += sendIdentityPacket(*identityPacketList, otherNodeData, *destinationNode); - - // remember the last time we sent identity details about this other node to the receiver - nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow()); - } - } - - QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getLocalID()); - - const bool distanceAdjust = true; - const bool dropFaceTracking = false; - AvatarDataPacket::SendStatus sendStatus; - sendStatus.sendUUID = true; - - do { - auto startSerialize = chrono::high_resolution_clock::now(); - QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, - sendStatus, dropFaceTracking, distanceAdjust, myPosition, - &lastSentJointsForOther, avatarSpaceAvailable); - auto endSerialize = chrono::high_resolution_clock::now(); - _stats.toByteArrayElapsedTime += - (quint64)chrono::duration_cast(endSerialize - startSerialize).count(); - - avatarPacket->write(bytes); - avatarSpaceAvailable -= bytes.size(); - numAvatarDataBytes += bytes.size(); - if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { - // Weren't able to fit everything. - nodeList->sendPacket(std::move(avatarPacket), *destinationNode); - ++numPacketsSent; - avatarPacket = NLPacket::create(PacketType::BulkAvatarData); - avatarSpaceAvailable = avatarPacketCapacity; - } - } while (!sendStatus); - - if (detail != AvatarData::NoData) { - _stats.numOthersIncluded++; - - // increment the number of avatars sent to this receiver - nodeData->incrementNumAvatarsSentLastFrame(); - - // set the last sent sequence number for this sender on the receiver - nodeData->setLastBroadcastSequenceNumber(otherNode->getLocalID(), - otherNodeData->getLastReceivedSequenceNumber()); - nodeData->setLastOtherAvatarEncodeTime(otherNode->getLocalID(), usecTimestampNow()); - } - - auto endAvatarDataPacking = chrono::high_resolution_clock::now(); - _stats.avatarDataPackingElapsedTime += - (quint64) chrono::duration_cast(endAvatarDataPacking - startAvatarDataPacking).count(); - - if (!overBudget) { - // use helper to add any changed traits to our packet list - traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList); - } - - remainingAvatars--; } if (nodeData->getNumAvatarsSentLastFrame() > numToSendEst) { diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index 91bb02fd55..8c5ad6b181 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -32,6 +32,7 @@ public: int numIdentityPacketsSent { 0 }; int numOthersIncluded { 0 }; int overBudgetAvatars { 0 }; + int numHeroesIncluded { 0 }; quint64 ignoreCalculationElapsedTime { 0 }; quint64 avatarDataPackingElapsedTime { 0 }; @@ -57,6 +58,7 @@ public: numIdentityPacketsSent = 0; numOthersIncluded = 0; overBudgetAvatars = 0; + numHeroesIncluded = 0; ignoreCalculationElapsedTime = 0; avatarDataPackingElapsedTime = 0; @@ -80,6 +82,7 @@ public: numIdentityPacketsSent += rhs.numIdentityPacketsSent; numOthersIncluded += rhs.numOthersIncluded; overBudgetAvatars += rhs.overBudgetAvatars; + numHeroesIncluded += rhs.numHeroesIncluded; ignoreCalculationElapsedTime += rhs.ignoreCalculationElapsedTime; avatarDataPackingElapsedTime += rhs.avatarDataPackingElapsedTime; @@ -90,9 +93,13 @@ public: } }; +class EntityTree; +using EntityTreePointer = std::shared_ptr; + struct SlaveSharedData { QStringList skeletonURLWhitelist; QUrl skeletonReplacementURL; + EntityTreePointer entityTree; }; class AvatarMixerSlave { diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h new file mode 100644 index 0000000000..4781fdee1b --- /dev/null +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -0,0 +1,31 @@ +// +// MixerAvatar.h +// assignment-client/src/avatars +// +// Created by Simon Walton Feb 2019. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Avatar class for use within the avatar mixer - encapsulates data required only for +// sorting priorities within the mixer. + +#ifndef hifi_MixerAvatar_h +#define hifi_MixerAvatar_h + +#include + +class MixerAvatar : public AvatarData { +public: + bool getPriorityAvatar() const { return _bPriorityAvatar; } + void setPriorityAvatar(bool bPriorityAvatar) { _bPriorityAvatar = bPriorityAvatar; } + +private: + bool _bPriorityAvatar { false }; +}; + +using MixerAvatarSharedPointer = std::shared_ptr; + +#endif // hifi_MixerAvatar_h diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index e993bea358..477d3dd612 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1203,7 +1203,8 @@ void OctreeServer::beginRunning() { auto nodeList = DependencyManager::get(); // we need to ask the DS about agents so we can ping/reply with them - nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); + nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer, + NodeType::AvatarMixer }); beforeRun(); // after payload has been processed diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 199fae77bf..7848c46eee 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -503,6 +503,7 @@ void OtherAvatar::handleChangedAvatarEntityData() { // then set the the original ID for the changes to take effect // TODO: This is a horrible hack and once properties.constructFromBuffer no longer causes // side effects...remove the following three lines + const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); entity->setParentID(NULL_ID); entity->setParentID(oldParentID); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 5da1c05aa2..d9614a0918 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -41,6 +41,7 @@ #include "EntitySimulation.h" #include "EntityDynamicFactoryInterface.h" +//#define WANT_DEBUG Q_DECLARE_METATYPE(EntityItemPointer); int entityItemPointernMetaTypeId = qRegisterMetaType(); @@ -95,6 +96,8 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_LAST_EDITED_BY; requestedProperties += PROP_ENTITY_HOST_TYPE; requestedProperties += PROP_OWNING_AVATAR_ID; + requestedProperties += PROP_PARENT_ID; + requestedProperties += PROP_PARENT_JOINT_INDEX; requestedProperties += PROP_QUERY_AA_CUBE; requestedProperties += PROP_CAN_CAST_SHADOW; requestedProperties += PROP_VISIBLE_IN_SECONDARY_CAMERA; @@ -502,6 +505,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } #ifdef WANT_DEBUG + { quint64 lastEdited = getLastEdited(); float editedAgo = getEditedAgo(); QString agoAsString = formatSecondsElapsed(editedAgo); @@ -515,6 +519,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef qCDebug(entities) << " age=" << getAge() << "seconds - " << ageAsString; qCDebug(entities) << " lastEdited =" << lastEdited; qCDebug(entities) << " ago=" << editedAgo << "seconds - " << agoAsString; + } #endif quint64 lastEditedFromBuffer = 0; @@ -1099,7 +1104,7 @@ void EntityItem::simulate(const quint64& now) { qCDebug(entities) << " hasGravity=" << hasGravity(); qCDebug(entities) << " hasAcceleration=" << hasAcceleration(); qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); - qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); + qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity(); qCDebug(entities) << " isMortal=" << isMortal(); qCDebug(entities) << " getAge()=" << getAge(); qCDebug(entities) << " getLifetime()=" << getLifetime(); @@ -1111,12 +1116,12 @@ void EntityItem::simulate(const quint64& now) { qCDebug(entities) << " hasGravity=" << hasGravity(); qCDebug(entities) << " hasAcceleration=" << hasAcceleration(); qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); - qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); + qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity(); } if (hasAngularVelocity()) { qCDebug(entities) << " CHANGING...="; qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); - qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); + qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity(); } if (isMortal()) { qCDebug(entities) << " MORTAL...="; @@ -1738,7 +1743,7 @@ bool EntityItem::contains(const glm::vec3& point) const { // the above cases not yet supported --> fall through to BOX case case SHAPE_TYPE_BOX: { localPoint = glm::abs(localPoint); - return glm::any(glm::lessThanEqual(localPoint, glm::vec3(NORMALIZED_HALF_SIDE))); + return glm::all(glm::lessThanEqual(localPoint, glm::vec3(NORMALIZED_HALF_SIDE))); } case SHAPE_TYPE_ELLIPSOID: { // since we've transformed into the normalized space this is just a sphere-point intersection test @@ -2652,13 +2657,23 @@ bool EntityItem::matchesJSONFilters(const QJsonObject& jsonFilters) const { // ALL entity properties. Some work will need to be done to the property system so that it can be more flexible // (to grab the value and default value of a property given the string representation of that property, for example) - // currently the only property filter we handle is '+' for serverScripts + // currently the only property filter we handle in EntityItem is '+' for serverScripts // which means that we only handle a filtered query asking for entities where the serverScripts property is non-default static const QString SERVER_SCRIPTS_PROPERTY = "serverScripts"; + static const QString ENTITY_TYPE_PROPERTY = "type"; - if (jsonFilters[SERVER_SCRIPTS_PROPERTY] == EntityQueryFilterSymbol::NonDefault) { - return _serverScripts != ENTITY_ITEM_DEFAULT_SERVER_SCRIPTS; + foreach(const auto& property, jsonFilters.keys()) { + if (property == SERVER_SCRIPTS_PROPERTY && jsonFilters[property] == EntityQueryFilterSymbol::NonDefault) { + // check if this entity has a non-default value for serverScripts + if (_serverScripts != ENTITY_ITEM_DEFAULT_SERVER_SCRIPTS) { + return true; + } else { + return false; + } + } else if (property == ENTITY_TYPE_PROPERTY) { + return (jsonFilters[property] == EntityTypes::getEntityTypeName(getType()) ); + } } // the json filter syntax did not match what we expected, return a match diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index bd1977f053..3fbfde6b24 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -514,7 +514,7 @@ public: QUuid getLastEditedBy() const { return _lastEditedBy; } void setLastEditedBy(QUuid value) { _lastEditedBy = value; } - bool matchesJSONFilters(const QJsonObject& jsonFilters) const; + virtual bool matchesJSONFilters(const QJsonObject& jsonFilters) const; virtual bool getMeshes(MeshProxyList& result) { return true; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5ae9b30869..d1a79c613d 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -617,6 +617,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed); CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed); CHECK_PROPERTY_CHANGE(PROP_FILTER_URL, filterURL); + CHECK_PROPERTY_CHANGE(PROP_AVATAR_PRIORITY, avatarPriority); CHECK_PROPERTY_CHANGE(PROP_KEY_LIGHT_MODE, keyLightMode); CHECK_PROPERTY_CHANGE(PROP_AMBIENT_LIGHT_MODE, ambientLightMode); CHECK_PROPERTY_CHANGE(PROP_SKYBOX_MODE, skyboxMode); @@ -1426,7 +1427,11 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {string} filterURL="" - The URL of a JavaScript file that filters changes to properties of entities within the * zone. It is periodically executed for each entity in the zone. It can, for example, be used to not allow changes to * certain properties.
+ * + * @property {boolean} avatarPriority=false - If true avatars within this zone will have their movements distributed to other + * clients with priority over other avatars. Use, for example, on a performance stage with a few presenters. *
+ *
  * function filter(properties) {
  *     // Test and edit properties object values,
  *     // e.g., properties.modelURL, as required.
@@ -1755,6 +1760,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FLYING_ALLOWED, flyingAllowed);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GHOSTING_ALLOWED, ghostingAllowed);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FILTER_URL, filterURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_AVATAR_PRIORITY, avatarPriority);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_KEY_LIGHT_MODE, keyLightMode, getKeyLightModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_AMBIENT_LIGHT_MODE, ambientLightMode, getAmbientLightModeAsString());
@@ -2118,6 +2124,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
     COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(filterURL, QString, setFilterURL);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE(avatarPriority, bool, setAvatarPriority);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(keyLightMode, KeyLightMode);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(ambientLightMode, AmbientLightMode);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(skyboxMode, SkyboxMode);
@@ -2398,6 +2405,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
     COPY_PROPERTY_IF_CHANGED(flyingAllowed);
     COPY_PROPERTY_IF_CHANGED(ghostingAllowed);
     COPY_PROPERTY_IF_CHANGED(filterURL);
+    COPY_PROPERTY_IF_CHANGED(avatarPriority);
     COPY_PROPERTY_IF_CHANGED(keyLightMode);
     COPY_PROPERTY_IF_CHANGED(ambientLightMode);
     COPY_PROPERTY_IF_CHANGED(skyboxMode);
@@ -2784,6 +2792,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         ADD_PROPERTY_TO_MAP(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool);
         ADD_PROPERTY_TO_MAP(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool);
         ADD_PROPERTY_TO_MAP(PROP_FILTER_URL, FilterURL, filterURL, QString);
+        ADD_PROPERTY_TO_MAP(PROP_AVATAR_PRIORITY, AvatarPriority, avatarPriority, bool);
         ADD_PROPERTY_TO_MAP(PROP_KEY_LIGHT_MODE, KeyLightMode, keyLightMode, uint32_t);
         ADD_PROPERTY_TO_MAP(PROP_AMBIENT_LIGHT_MODE, AmbientLightMode, ambientLightMode, uint32_t);
         ADD_PROPERTY_TO_MAP(PROP_SKYBOX_MODE, SkyboxMode, skyboxMode, uint32_t);
@@ -3191,6 +3200,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
                 APPEND_ENTITY_PROPERTY(PROP_SKYBOX_MODE, (uint32_t)properties.getSkyboxMode());
                 APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)properties.getHazeMode());
                 APPEND_ENTITY_PROPERTY(PROP_BLOOM_MODE, (uint32_t)properties.getBloomMode());
+                APPEND_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, properties.getAvatarPriority());
             }
 
             if (properties.getType() == EntityTypes::PolyVox) {
@@ -3650,6 +3660,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FLYING_ALLOWED, bool, setFlyingAllowed);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FILTER_URL, QString, setFilterURL);
+        READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_AVATAR_PRIORITY, bool, setAvatarPriority);
 
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_KEY_LIGHT_MODE, uint32_t, setKeyLightMode);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_AMBIENT_LIGHT_MODE, uint32_t, setAmbientLightMode);
@@ -4033,6 +4044,7 @@ void EntityItemProperties::markAllChanged() {
     _bloom.markAllChanged();
     _flyingAllowedChanged = true;
     _ghostingAllowedChanged = true;
+    _avatarPriorityChanged = true;
     _filterURLChanged = true;
     _keyLightModeChanged = true;
     _ambientLightModeChanged = true;
@@ -4622,6 +4634,9 @@ QList EntityItemProperties::listChangedProperties() {
     if (filterURLChanged()) {
         out += "filterURL";
     }
+    if (avatarPriorityChanged()) {
+        out += "avatarPriority";
+    }
     if (keyLightModeChanged()) {
         out += "keyLightMode";
     }
diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h
index 00a93dd129..be4a25054f 100644
--- a/libraries/entities/src/EntityItemProperties.h
+++ b/libraries/entities/src/EntityItemProperties.h
@@ -316,6 +316,7 @@ public:
     DEFINE_PROPERTY(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool, ZoneEntityItem::DEFAULT_FLYING_ALLOWED);
     DEFINE_PROPERTY(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool, ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED);
     DEFINE_PROPERTY(PROP_FILTER_URL, FilterURL, filterURL, QString, ZoneEntityItem::DEFAULT_FILTER_URL);
+    DEFINE_PROPERTY(PROP_AVATAR_PRIORITY, AvatarPriority, avatarPriority, bool, ZoneEntityItem::DEFAULT_AVATAR_PRIORITY);
     DEFINE_PROPERTY_REF_ENUM(PROP_KEY_LIGHT_MODE, KeyLightMode, keyLightMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
     DEFINE_PROPERTY_REF_ENUM(PROP_SKYBOX_MODE, SkyboxMode, skyboxMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
     DEFINE_PROPERTY_REF_ENUM(PROP_AMBIENT_LIGHT_MODE, AmbientLightMode, ambientLightMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
@@ -681,6 +682,8 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, "");
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, FilterURL, filterURL, "");
 
+    DEBUG_PROPERTY_IF_CHANGED(debug, properties, AvatarPriority, avatarPriority, "");
+
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, EntityHostTypeAsString, entityHostType, "");
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, "");
 
diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h
index 0326268f55..6d1c3a1df8 100644
--- a/libraries/entities/src/EntityPropertyFlags.h
+++ b/libraries/entities/src/EntityPropertyFlags.h
@@ -156,6 +156,7 @@ enum EntityPropertyList {
     PROP_DERIVED_28,
     PROP_DERIVED_29,
     PROP_DERIVED_30,
+    PROP_DERIVED_31,
 
     PROP_AFTER_LAST_ITEM,
 
@@ -276,6 +277,8 @@ enum EntityPropertyList {
     PROP_SKYBOX_MODE = PROP_DERIVED_28,
     PROP_HAZE_MODE = PROP_DERIVED_29,
     PROP_BLOOM_MODE = PROP_DERIVED_30,
+    // Avatar priority
+    PROP_AVATAR_PRIORITY = PROP_DERIVED_31,
 
     // Polyvox
     PROP_VOXEL_VOLUME_SIZE = PROP_DERIVED_0,
diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp
index 7b0491dbc0..ebe7046812 100644
--- a/libraries/entities/src/ZoneEntityItem.cpp
+++ b/libraries/entities/src/ZoneEntityItem.cpp
@@ -65,6 +65,7 @@ EntityItemProperties ZoneEntityItem::getProperties(const EntityPropertyFlags& de
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(filterURL, getFilterURL);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(avatarPriority, getAvatarPriority);
 
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(keyLightMode, getKeyLightMode);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(ambientLightMode, getAmbientLightMode);
@@ -117,6 +118,7 @@ bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& propertie
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(skyboxMode, setSkyboxMode);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(hazeMode, setHazeMode);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(bloomMode, setBloomMode);
+    SET_ENTITY_PROPERTY_FROM_PROPERTIES(avatarPriority, setAvatarPriority);
 
     somethingChanged = somethingChanged || _keyLightPropertiesChanged || _ambientLightPropertiesChanged ||
         _skyboxPropertiesChanged || _hazePropertiesChanged || _bloomPropertiesChanged;
@@ -192,6 +194,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
     READ_ENTITY_PROPERTY(PROP_SKYBOX_MODE, uint32_t, setSkyboxMode);
     READ_ENTITY_PROPERTY(PROP_HAZE_MODE, uint32_t, setHazeMode);
     READ_ENTITY_PROPERTY(PROP_BLOOM_MODE, uint32_t, setBloomMode);
+    READ_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, bool, setAvatarPriority);
 
     return bytesRead;
 }
@@ -211,6 +214,7 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p
     requestedProperties += PROP_FLYING_ALLOWED;
     requestedProperties += PROP_GHOSTING_ALLOWED;
     requestedProperties += PROP_FILTER_URL;
+    requestedProperties += PROP_AVATAR_PRIORITY;
 
     requestedProperties += PROP_KEY_LIGHT_MODE;
     requestedProperties += PROP_AMBIENT_LIGHT_MODE;
@@ -256,6 +260,7 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits
     APPEND_ENTITY_PROPERTY(PROP_SKYBOX_MODE, (uint32_t)getSkyboxMode());
     APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)getHazeMode());
     APPEND_ENTITY_PROPERTY(PROP_BLOOM_MODE, (uint32_t)getBloomMode());
+    APPEND_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, getAvatarPriority());
 }
 
 void ZoneEntityItem::debugDump() const {
@@ -269,6 +274,7 @@ void ZoneEntityItem::debugDump() const {
     qCDebug(entities) << "   _ambientLightMode:" << EntityItemProperties::getComponentModeAsString(_ambientLightMode);
     qCDebug(entities) << "         _skyboxMode:" << EntityItemProperties::getComponentModeAsString(_skyboxMode);
     qCDebug(entities) << "          _bloomMode:" << EntityItemProperties::getComponentModeAsString(_bloomMode);
+    qCDebug(entities) << "     _avatarPriority:" << getAvatarPriority();
 
     _keyLightProperties.debugDump();
     _ambientLightProperties.debugDump();
@@ -463,3 +469,16 @@ void ZoneEntityItem::fetchCollisionGeometryResource() {
         _shapeResource = DependencyManager::get()->getCollisionGeometryResource(hullURL);
     }
 }
+
+bool ZoneEntityItem::matchesJSONFilters(const QJsonObject& jsonFilters) const {
+    // currently the only property filter we handle in ZoneEntityItem is value of avatarPriority
+
+    static const QString AVATAR_PRIORITY_PROPERTY = "avatarPriority";
+
+    if (jsonFilters.contains(AVATAR_PRIORITY_PROPERTY)) {
+        return (jsonFilters[AVATAR_PRIORITY_PROPERTY].toBool() == _avatarPriority);
+    }
+
+    // Chain to base:
+    return EntityItem::matchesJSONFilters(jsonFilters);
+}
diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h
index 11c85dab89..acf3cfdddf 100644
--- a/libraries/entities/src/ZoneEntityItem.h
+++ b/libraries/entities/src/ZoneEntityItem.h
@@ -66,6 +66,8 @@ public:
     QString getCompoundShapeURL() const;
     virtual void setCompoundShapeURL(const QString& url);
 
+    virtual bool matchesJSONFilters(const QJsonObject& jsonFilters) const override;
+
     KeyLightPropertyGroup getKeyLightProperties() const { return resultWithReadLock([&] { return _keyLightProperties; }); }
     AmbientLightPropertyGroup getAmbientLightProperties() const { return resultWithReadLock([&] { return _ambientLightProperties; }); }
 
@@ -96,6 +98,9 @@ public:
     QString getFilterURL() const;
     void setFilterURL(const QString url); 
 
+    bool getAvatarPriority() const { return _avatarPriority; }
+    void setAvatarPriority(bool value) { _avatarPriority = value; }
+
     bool keyLightPropertiesChanged() const { return _keyLightPropertiesChanged; }
     bool ambientLightPropertiesChanged() const { return _ambientLightPropertiesChanged; }
     bool skyboxPropertiesChanged() const { return _skyboxPropertiesChanged; }
@@ -123,6 +128,7 @@ public:
     static const bool DEFAULT_FLYING_ALLOWED;
     static const bool DEFAULT_GHOSTING_ALLOWED;
     static const QString DEFAULT_FILTER_URL;
+    static const bool DEFAULT_AVATAR_PRIORITY = false;
 
 protected:
     KeyLightPropertyGroup _keyLightProperties;
@@ -147,6 +153,9 @@ protected:
     bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED };
     QString _filterURL { DEFAULT_FILTER_URL };
 
+    // Avatar-updates priority
+    bool _avatarPriority { DEFAULT_AVATAR_PRIORITY };
+
     // Dirty flags turn true when either keylight properties is changing values.
     bool _keyLightPropertiesChanged { false };
     bool _ambientLightPropertiesChanged { false };
diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp
index bdba47f0ed..012ac242ab 100644
--- a/libraries/networking/src/ThreadedAssignment.cpp
+++ b/libraries/networking/src/ThreadedAssignment.cpp
@@ -123,6 +123,9 @@ void ThreadedAssignment::checkInWithDomainServerOrExit() {
 
         // increase the number of queued check ins
         _numQueuedCheckIns++;
+        if (_numQueuedCheckIns > 1) {
+            qCDebug(networking) << "Number of queued checkins = " << _numQueuedCheckIns;
+        }
     }
 }
 
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index 4229a4a152..1489f8e16c 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -260,6 +260,7 @@ enum class EntityVersion : PacketVersion {
     MissingWebEntityProperties,
     PulseProperties,
     RingGizmoEntities,
+    AvatarPriorityZone,
     ShowKeyboardFocusHighlight,
     WebBillboardMode,
     ModelScale,
diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json
index 4c78da7306..7e5f2c8659 100644
--- a/scripts/system/assets/data/createAppTooltips.json
+++ b/scripts/system/assets/data/createAppTooltips.json
@@ -134,6 +134,9 @@
     "bloom.bloomSize": {
         "tooltip": "The radius of bloom. The higher the value, the larger the bloom."
     },
+    "avatarPriority": {
+        "tooltip":  "Avatars in this zone will have a higher update priority."
+    },
     "modelURL": {
         "tooltip": "A mesh model from an FBX or OBJ file."
     },
diff --git a/scripts/system/edit.js b/scripts/system/edit.js
index 9d807264aa..67a789c266 100644
--- a/scripts/system/edit.js
+++ b/scripts/system/edit.js
@@ -382,7 +382,8 @@ const DEFAULT_ENTITY_PROPERTIES = {
             },
         },
         shapeType: "box",
-        bloomMode: "inherit"
+        bloomMode: "inherit",
+        avatarPriority: false
     },
     Model: {
         collisionShape: "none",
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js
index c1a8f363b5..062b9a392a 100644
--- a/scripts/system/html/js/entityProperties.js
+++ b/scripts/system/html/js/entityProperties.js
@@ -428,6 +428,12 @@ const GROUPS = [
                 propertyID: "bloom.bloomSize",
                 showPropertyRule: { "bloomMode": "enabled" },
             },
+            {
+                label: "Avatar Priority",
+                type: "bool",
+                propertyID: "avatarPriority",
+            },
+
         ]
     },
     {