diff --git a/assignment-client/src/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp index 7cc7ac9f93..78efb98b37 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.cpp +++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp @@ -64,10 +64,6 @@ bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) { return _pool._queue.try_pop(node); } -#ifdef AUDIO_SINGLE_THREADED -static AudioMixerSlave slave; -#endif - void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) { _function = &AudioMixerSlave::processPackets; _configure = [](AudioMixerSlave& slave) {}; @@ -87,19 +83,9 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) { _begin = begin; _end = end; -#ifdef AUDIO_SINGLE_THREADED - _configure(slave); - std::for_each(begin, end, [&](const SharedNodePointer& node) { - _function(slave, node); - }); -#else // fill the queue std::for_each(_begin, _end, [&](const SharedNodePointer& node) { -#if defined(__clang__) && defined(Q_OS_LINUX) _queue.push(node); -#else - _queue.emplace(node); -#endif }); { @@ -119,17 +105,12 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) { } assert(_queue.empty()); -#endif } void AudioMixerSlavePool::each(std::function functor) { -#ifdef AUDIO_SINGLE_THREADED - functor(slave); -#else for (auto& slave : _slaves) { functor(*slave.get()); } -#endif } void AudioMixerSlavePool::setNumThreads(int numThreads) { @@ -155,9 +136,6 @@ void AudioMixerSlavePool::setNumThreads(int numThreads) { void AudioMixerSlavePool::resize(int numThreads) { assert(_numThreads == (int)_slaves.size()); -#ifdef AUDIO_SINGLE_THREADED - qDebug("%s: running single threaded", __FUNCTION__, numThreads); -#else qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads); Lock lock(_mutex); @@ -205,5 +183,4 @@ void AudioMixerSlavePool::resize(int numThreads) { _numThreads = _numStarted = _numFinished = numThreads; assert(_numThreads == (int)_slaves.size()); -#endif } diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 801f28c6f5..e4077d5d46 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(); @@ -252,6 +264,10 @@ void AvatarMixer::start() { }, &lockWait, &nodeTransform, &functor); auto end = usecTimestampNow(); _processQueuedAvatarDataPacketsElapsedTime += (end - start); + + _broadcastAvatarDataLockWait += lockWait; + _broadcastAvatarDataNodeTransform += nodeTransform; + _broadcastAvatarDataNodeFunctor += functor; } // process pending display names... this doesn't currently run on multiple threads, because it @@ -269,6 +285,10 @@ void AvatarMixer::start() { }, &lockWait, &nodeTransform, &functor); auto end = usecTimestampNow(); _displayNameManagementElapsedTime += (end - start); + + _broadcastAvatarDataLockWait += lockWait; + _broadcastAvatarDataNodeTransform += nodeTransform; + _broadcastAvatarDataNodeFunctor += functor; } // this is where we need to put the real work... @@ -691,8 +711,11 @@ void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer(); 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(); } @@ -939,6 +965,14 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { qCDebug(avatars) << "Avatar mixer will automatically determine number of threads to use. Using:" << _slavePool.numThreads() << "threads."; } + { + const QString CONNECTION_RATE = "connection_rate"; + auto nodeList = DependencyManager::get(); + auto defaultConnectionRate = nodeList->getMaxConnectionRate(); + int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate); + nodeList->setMaxConnectionRate(connectionRate); + } + const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; @@ -976,3 +1010,58 @@ 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: + { // Ignore stats, but may have a different Entity packet appended. + OctreeHeadlessViewer::parseOctreeStats(message, senderNode); + const auto piggyBackedSizeWithHeader = message->getBytesLeftToRead(); + if (piggyBackedSizeWithHeader > 0) { + // pull out the piggybacked packet and create a new QSharedPointer for it + auto buffer = std::unique_ptr(new char[piggyBackedSizeWithHeader]); + memcpy(buffer.get(), message->getRawMessage() + message->getPosition(), piggyBackedSizeWithHeader); + + auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggyBackedSizeWithHeader, message->getSenderSockAddr()); + auto newMessage = QSharedPointer::create(*newPacket); + handleOctreePacket(newMessage, senderNode); + } + 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..cef4383aee 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,42 @@ 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 }; + float zoneVolume { std::numeric_limits::max() }; + + 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)) { + auto zoneItem = static_pointer_cast(item); + if (zoneItem->getAvatarPriority() != COMPONENT_MODE_INHERIT) { + float volume = zoneItem->getVolumeEstimate(); + if (volume < findPriorityZone->zoneVolume) { // Smaller volume wins + findPriorityZone->isInPriorityZone = zoneItem->getAvatarPriority() == COMPONENT_MODE_ENABLED; + findPriorityZone->zoneVolume = volume; + } + } + } + }); + return true; // Keep recursing + } 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 +129,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->setHasPriority(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..e59c81f4b7 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(); @@ -293,29 +320,30 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) _stats.nodesBroadcastedTo++; - AvatarMixerClientData* nodeData = reinterpret_cast(destinationNode->getLinkedData()); + AvatarMixerClientData* destinationNodeData = reinterpret_cast(destinationNode->getLinkedData()); - nodeData->resetInViewStats(); + destinationNodeData->resetInViewStats(); - const AvatarData& avatar = nodeData->getAvatar(); - glm::vec3 myPosition = avatar.getClientGlobalPosition(); + const AvatarData& avatar = destinationNodeData->getAvatar(); + glm::vec3 destinationPosition = avatar.getClientGlobalPosition(); // reset the internal state for correct random number distribution distribution.reset(); // Estimate number to sort on number sent last frame (with min. of 20). - const int numToSendEst = std::max(int(nodeData->getNumAvatarsSentLastFrame() * 2.5f), 20); + const int numToSendEst = std::max(int(destinationNodeData->getNumAvatarsSentLastFrame() * 2.5f), 20); // reset the number of sent avatars - nodeData->resetNumAvatarsSentLastFrame(); + destinationNodeData->resetNumAvatarsSentLastFrame(); // keep track of outbound data rate specifically for avatar data int numAvatarDataBytes = 0; 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; @@ -325,8 +353,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // When this is true, the AvatarMixer will send Avatar data to a client // about avatars they've ignored or that are out of view - bool PALIsOpen = nodeData->getRequestsDomainListData(); - bool PALWasOpen = nodeData->getPrevRequestsDomainListData(); + bool PALIsOpen = destinationNodeData->getRequestsDomainListData(); + bool PALWasOpen = destinationNodeData->getPrevRequestsDomainListData(); // When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them bool getsAnyIgnored = PALIsOpen && destinationNode->getCanKick(); @@ -337,36 +365,23 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // compute node bounding box 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; - }; + AABox destinationNodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR); // prepare to sort - const auto& cameraViews = nodeData->getViewFrustums(); - PrioritySortUtil::PriorityQueue sortedAvatars(cameraViews, - AvatarData::_avatarSortCoefficientSize, - AvatarData::_avatarSortCoefficientCenter, - AvatarData::_avatarSortCoefficientAge); - sortedAvatars.reserve(_end - _begin); + const auto& cameraViews = destinationNodeData->getViewFrustums(); + + 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(); @@ -376,47 +391,47 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) continue; } - auto avatarNode = otherNodeRaw; + auto sourceAvatarNode = otherNodeRaw; - bool shouldIgnore = false; + bool sendAvatar = true; // We will consider this source avatar for sending. // We ignore other nodes for a couple of reasons: // 1) ignore bubbles and ignore specific node // 2) the node hasn't really updated it's frame data recently, this can // happen if for example the avatar is connected on a desktop and sending // updates at ~30hz. So every 3 frames we skip a frame. - assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map + assert(sourceAvatarNode); // we can't have gotten here without the avatarData being a valid key in the map - const AvatarMixerClientData* avatarClientNodeData = reinterpret_cast(avatarNode->getLinkedData()); - assert(avatarClientNodeData); // we can't have gotten here without avatarNode having valid data + const AvatarMixerClientData* sourceAvatarNodeData = reinterpret_cast(sourceAvatarNode->getLinkedData()); + assert(sourceAvatarNodeData); // we can't have gotten here without sourceAvatarNode having valid data quint64 startIgnoreCalculation = usecTimestampNow(); // make sure we have data for this avatar, that it isn't the same node, // and isn't an avatar that the viewing node has ignored // or that has ignored the viewing node - if ((destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen) - || (avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) { - shouldIgnore = true; + if ((destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) && !PALIsOpen) + || (sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) { + sendAvatar = false; } else { // Check to see if the space bubble is enabled // Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored - if (nodeData->isIgnoreRadiusEnabled() || (avatarClientNodeData->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { + if (destinationNodeData->isIgnoreRadiusEnabled() || (sourceAvatarNodeData->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { // Perform the collision check between the two bounding boxes - AABox otherNodeBox = avatarClientNodeData->getAvatar().getDefaultBubbleBox(); - if (nodeBox.touches(otherNodeBox)) { - nodeData->ignoreOther(destinationNode, avatarNode); - shouldIgnore = !getsAnyIgnored; + AABox sourceNodeBox = sourceAvatarNodeData->getAvatar().getDefaultBubbleBox(); + if (destinationNodeBox.touches(sourceNodeBox)) { + destinationNodeData->ignoreOther(destinationNode, sourceAvatarNode); + sendAvatar = getsAnyIgnored; } } // Not close enough to ignore - if (!shouldIgnore) { - nodeData->removeFromRadiusIgnoringSet(avatarNode->getUUID()); + if (sendAvatar) { + destinationNodeData->removeFromRadiusIgnoringSet(sourceAvatarNode->getUUID()); } } - if (!shouldIgnore) { - AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getLocalID()); - AvatarDataSequenceNumber lastSeqFromSender = avatarClientNodeData->getLastReceivedSequenceNumber(); + if (sendAvatar) { + AvatarDataSequenceNumber lastSeqToReceiver = destinationNodeData->getLastBroadcastSequenceNumber(sourceAvatarNode->getLocalID()); + AvatarDataSequenceNumber lastSeqFromSender = sourceAvatarNodeData->getLastReceivedSequenceNumber(); // FIXME - This code does appear to be working. But it seems brittle. // It supports determining if the frame of data for this "other" @@ -430,26 +445,28 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // or that somehow we haven't sent if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) { ++numAvatarsHeldBack; - shouldIgnore = true; + sendAvatar = false; } else if (lastSeqFromSender == 0) { - // We have have not yet recieved any data about this avatar. Ignore it for now + // We have have not yet received any data about this avatar. Ignore it for now // This is important for Agent scripts that are not avatar // so that they don't appear to be an avatar at the origin - shouldIgnore = true; + sendAvatar = false; } else if (lastSeqFromSender - lastSeqToReceiver > 1) { // this is a skip - we still send the packet but capture the presence of the skip so we see it happening ++numAvatarsWithSkippedFrames; } } + quint64 endIgnoreCalculation = usecTimestampNow(); _stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation); - if (!shouldIgnore) { + if (sendAvatar) { // sort this one for later - const AvatarData* avatarNodeData = avatarClientNodeData->getConstAvatarData(); - auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNode->getLocalID()); + const MixerAvatar* avatarNodeData = sourceAvatarNodeData->getConstAvatarData(); + auto lastEncodeTime = destinationNodeData->getLastOtherAvatarEncodeTime(sourceAvatarNode->getLocalID()); - sortedAvatars.push(SortableAvatar(avatarNodeData, avatarNode, lastEncodeTime)); + avatarPriorityQueues[avatarNodeData->getHasPriority() ? kHero : kNonhero].push( + SortableAvatar(avatarNodeData, sourceAvatarNode, lastEncodeTime)); } // If Avatar A's PAL WAS open but is no longer open, AND @@ -459,135 +476,153 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // will be sent when it doesn't need to be (but where it _should_ be OK to send). // However, it's less heavy-handed than using `shouldIgnore`. if (PALWasOpen && !PALIsOpen && - (destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) || - avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) { + (destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) || + sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) { // ...send a Kill Packet to Node A, instructing Node A to kill Avatar B, // then have Node A cleanup the killed Node B. auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true); - packet->write(avatarNode->getUUID().toRfc4122()); + packet->write(sourceAvatarNode->getUUID().toRfc4122()); packet->writePrimitive(KillAvatarReason::AvatarIgnored); nodeList->sendPacket(std::move(packet), *destinationNode); - nodeData->cleanupKilledNode(avatarNode->getUUID(), avatarNode->getLocalID()); + destinationNodeData->cleanupKilledNode(sourceAvatarNode->getUUID(), sourceAvatarNode->getLocalID()); } - nodeData->setPrevRequestsDomainListData(PALIsOpen); + destinationNodeData->setPrevRequestsDomainListData(PALIsOpen); } // 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(); + // Loop over two priorities - hero avatars then everyone else: + for (PriorityVariants currentVariant = kHero; currentVariant <= kNonhero; ++((int&)currentVariant)) { + const auto& sortedAvatarVector = avatarPriorityQueues[currentVariant].getSortedVector(numToSendEst); + for (const auto& sortedAvatar : sortedAvatarVector) { + const Node* sourceNode = 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(sourceNode); // 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* sourceNodeData = reinterpret_cast(sourceNode->getLinkedData()); + const MixerAvatar* sourceAvatar = sourceNodeData->getConstAvatarData(); + + // Typically all out-of-view avatars but such avatars' priorities will rise with time: + bool isLowerPriority = currentVariant != kHero && sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; // XXX: hero handling? + + if (isLowerPriority) { + detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData; + destinationNodeData->incrementAvatarOutOfView(); + } else if (!overBudget) { + detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; + destinationNodeData->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 (sourceAvatar->hasProcessedFirstIdentity() + && destinationNodeData->getLastBroadcastTime(sourceNode->getLocalID()) <= sourceNodeData->getIdentityChangeTimestamp()) { + identityBytesSent += sendIdentityPacket(*identityPacketList, sourceNodeData, *destinationNode); + + // remember the last time we sent identity details about this other node to the receiver + destinationNodeData->setLastBroadcastTime(sourceNode->getLocalID(), usecTimestampNow()); + } + } + + QVector& lastSentJointsForOther = destinationNodeData->getLastOtherAvatarSentJoints(sourceNode->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 = sourceAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, + sendStatus, dropFaceTracking, distanceAdjust, destinationPosition, + &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 (sourceAvatar->getHasPriority()) { + _stats.numHeroesIncluded++; + } + + // increment the number of avatars sent to this receiver + destinationNodeData->incrementNumAvatarsSentLastFrame(); + + // set the last sent sequence number for this sender on the receiver + destinationNodeData->setLastBroadcastSequenceNumber(sourceNode->getLocalID(), + sourceNodeData->getLastReceivedSequenceNumber()); + destinationNodeData->setLastOtherAvatarEncodeTime(sourceNode->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(destinationNodeData, sourceNodeData, *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) { - qCWarning(avatars) << "More avatars sent than upper estimate" << nodeData->getNumAvatarsSentLastFrame() + if (destinationNodeData->getNumAvatarsSentLastFrame() > numToSendEst) { + qCWarning(avatars) << "More avatars sent than upper estimate" << destinationNodeData->getNumAvatarsSentLastFrame() << " / " << numToSendEst; } @@ -618,12 +653,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) } // record the bytes sent for other avatar data in the AvatarMixerClientData - nodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent); + destinationNodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent); // record the number of avatars held back this frame - nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); - nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); + destinationNodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); + destinationNodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); quint64 endPacketSending = usecTimestampNow(); _stats.packetSendingElapsedTime += (endPacketSending - startPacketSending); 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/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index cf842ac792..013d914cbe 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -63,10 +63,6 @@ bool AvatarMixerSlaveThread::try_pop(SharedNodePointer& node) { return _pool._queue.try_pop(node); } -#ifdef AVATAR_SINGLE_THREADED -static AvatarMixerSlave slave; -#endif - void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) { _function = &AvatarMixerSlave::processIncomingPackets; _configure = [=](AvatarMixerSlave& slave) { @@ -89,19 +85,9 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) { _begin = begin; _end = end; -#ifdef AUDIO_SINGLE_THREADED - _configure(slave); - std::for_each(begin, end, [&](const SharedNodePointer& node) { - _function(slave, node); -}); -#else // fill the queue std::for_each(_begin, _end, [&](const SharedNodePointer& node) { -#if defined(__clang__) && defined(Q_OS_LINUX) _queue.push(node); -#else - _queue.emplace(node); -#endif }); { @@ -121,18 +107,13 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) { } assert(_queue.empty()); -#endif } void AvatarMixerSlavePool::each(std::function functor) { -#ifdef AVATAR_SINGLE_THREADED - functor(slave); -#else for (auto& slave : _slaves) { functor(*slave.get()); } -#endif } void AvatarMixerSlavePool::setNumThreads(int numThreads) { @@ -158,9 +139,6 @@ void AvatarMixerSlavePool::setNumThreads(int numThreads) { void AvatarMixerSlavePool::resize(int numThreads) { assert(_numThreads == (int)_slaves.size()); -#ifdef AVATAR_SINGLE_THREADED - qDebug("%s: running single threaded", __FUNCTION__, numThreads); -#else qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads); Lock lock(_mutex); @@ -208,5 +186,4 @@ void AvatarMixerSlavePool::resize(int numThreads) { _numThreads = _numStarted = _numFinished = numThreads; assert(_numThreads == (int)_slaves.size()); -#endif } diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h new file mode 100644 index 0000000000..4c3ded4582 --- /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 getHasPriority() const { return _hasPriority; } + void setHasPriority(bool hasPriority) { _hasPriority = hasPriority; } + +private: + bool _hasPriority { 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/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 49023c9af8..140c7d6c17 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1302,6 +1302,14 @@ "placeholder": "1", "default": "1", "advanced": true + }, + { + "name": "connection_rate", + "label": "Connection Rate", + "help": "Number of new agents that can connect to the mixer every second", + "placeholder": "50", + "default": "50", + "advanced": true } ] }, diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 85b116129c..8d5cb165cb 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1243,12 +1243,11 @@ void DomainServer::broadcastNewNode(const SharedNodePointer& addedNode) { limitedNodeList->eachMatchingNode( [this, addedNode](const SharedNodePointer& node)->bool { - if (node->getLinkedData() && node->getActiveSocket() && node != addedNode) { - // is the added Node in this node's interest list? - return isInInterestSet(node, addedNode); - } else { - return false; - } + // is the added Node in this node's interest list? + return node->getLinkedData() + && node->getActiveSocket() + && node != addedNode + && isInInterestSet(node, addedNode); }, [this, &addNodePacket, connectionSecretIndex, addedNode, limitedNodeListWeak](const SharedNodePointer& node) { // send off this packet to the node diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 85ef821a4a..bf8c06d1b3 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -213,6 +213,63 @@ Item { popup.open(); } + HiFiGlyphs { + id: errorsGlyph + visible: !AvatarPackagerCore.currentAvatarProject || AvatarPackagerCore.currentAvatarProject.hasErrors + text: hifi.glyphs.alert + size: 315 + color: "#EA4C5F" + anchors { + top: parent.top + topMargin: -30 + horizontalCenter: parent.horizontalCenter + } + } + + Image { + id: successGlyph + visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.hasErrors + anchors { + top: parent.top + topMargin: 52 + horizontalCenter: parent.horizontalCenter + } + width: 149.6 + height: 149 + source: "../../../icons/checkmark-stroke.svg" + } + + RalewayRegular { + id: doctorStatusMessage + + states: [ + State { + when: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.hasErrors + name: "noErrors" + PropertyChanges { + target: doctorStatusMessage + text: "Your avatar looks fine." + } + }, + State { + when: !AvatarPackagerCore.currentAvatarProject || AvatarPackagerCore.currentAvatarProject.hasErrors + name: "errors" + PropertyChanges { + target: doctorStatusMessage + text: "Your avatar has a few issues." + } + } + ] + color: 'white' + size: 20 + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: errorsGlyph.bottom + + wrapMode: Text.Wrap + } + RalewayRegular { id: infoMessage @@ -240,7 +297,7 @@ Item { anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top + anchors.top: doctorStatusMessage.bottom anchors.bottomMargin: 24 @@ -249,6 +306,53 @@ Item { text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users." } + RalewayRegular { + id: notForSaleMessage + + visible: root.hasSuccessfullyUploaded + + color: 'white' + linkColor: '#00B4EF' + size: 20 + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: infoMessage.bottom + anchors.topMargin: 10 + + anchors.bottomMargin: 24 + + wrapMode: Text.Wrap + text: "This item is not for sale yet, learn more." + + onLinkActivated: { + Qt.openUrlExternally("https://docs.highfidelity.com/sell/add-item/upload-avatar.html"); + } + } + + RalewayRegular { + id: showErrorsLink + + color: 'white' + linkColor: '#00B4EF' + + visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors + + anchors { + top: notForSaleMessage.bottom + topMargin: 16 + horizontalCenter: parent.horizontalCenter + } + + size: 28 + + text: "View all errors" + + onLinkActivated: { + avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport; + } + } + HifiControls.Button { id: openFolderButton diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index f11547bdca..0a63290051 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -77,7 +77,10 @@ public: return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath())); } Q_INVOKABLE bool getHasErrors() const { return _hasErrors; } - Q_INVOKABLE void setHasErrors(bool hasErrors) { _hasErrors = hasErrors; } + Q_INVOKABLE void setHasErrors(bool hasErrors) { + _hasErrors = hasErrors; + emit hasErrorsChanged(); + } /** * returns the AvatarProject or a nullptr on failure. 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/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 9d4c1645d3..0fe03c7074 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1066,13 +1066,6 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos if (_enableInverseKinematics) { _animVars.set("ikOverlayAlpha", 1.0f); - _animVars.set("splineIKEnabled", true); - _animVars.set("leftHandIKEnabled", true); - _animVars.set("rightHandIKEnabled", true); - _animVars.set("leftFootIKEnabled", true); - _animVars.set("rightFootIKEnabled", true); - _animVars.set("leftFootPoleVectorEnabled", true); - _animVars.set("rightFootPoleVectorEnabled", true); } else { _animVars.set("ikOverlayAlpha", 0.0f); _animVars.set("splineIKEnabled", false); @@ -1086,6 +1079,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _animVars.set("rightFootPoleVectorEnabled", false); } _lastEnableInverseKinematics = _enableInverseKinematics; + } _lastForward = forward; _lastPosition = worldPosition; 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..3d14206c95 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -225,6 +225,15 @@ QString EntityItemProperties::getBloomModeAsString() const { return getComponentModeAsString(_bloomMode); } +namespace { + const QStringList AVATAR_PRIORITIES_AS_STRING + { "inherit", "crowd", "hero" }; +} + +QString EntityItemProperties::getAvatarPriorityAsString() const { + return AVATAR_PRIORITIES_AS_STRING.value(_avatarPriority); +} + std::array::const_iterator EntityItemProperties::findComponent(const QString& mode) { return std::find_if(COMPONENT_MODES.begin(), COMPONENT_MODES.end(), [&](const ComponentPair& pair) { return (pair.second == mode); @@ -249,6 +258,15 @@ void EntityItemProperties::setBloomModeFromString(const QString& bloomMode) { } } +void EntityItemProperties::setAvatarPriorityFromString(QString const& avatarPriority) { + auto result = AVATAR_PRIORITIES_AS_STRING.indexOf(avatarPriority); + + if (result != -1) { + _avatarPriority = result; + _avatarPriorityChanged = true; + } +} + QString EntityItemProperties::getKeyLightModeAsString() const { return getComponentModeAsString(_keyLightMode); } @@ -622,6 +640,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_SKYBOX_MODE, skyboxMode); CHECK_PROPERTY_CHANGE(PROP_HAZE_MODE, hazeMode); CHECK_PROPERTY_CHANGE(PROP_BLOOM_MODE, bloomMode); + CHECK_PROPERTY_CHANGE(PROP_AVATAR_PRIORITY, avatarPriority); // Polyvox CHECK_PROPERTY_CHANGE(PROP_VOXEL_VOLUME_SIZE, voxelVolumeSize); @@ -1426,7 +1445,13 @@ 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 {string} avatarPriority="inherit" - Configures the update priority of contained avatars to other clients.
+ * "inherit": Priority from enclosing zones is unchanged.
+ * "crowd": Priority in this zone is the normal priority.
+ * "hero": Avatars in this zone will have an increased update priority *
+ *
  * function filter(properties) {
  *     // Test and edit properties object values,
  *     // e.g., properties.modelURL, as required.
@@ -1761,6 +1786,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SKYBOX_MODE, skyboxMode, getSkyboxModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_HAZE_MODE, hazeMode, getHazeModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BLOOM_MODE, bloomMode, getBloomModeAsString());
+        COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_AVATAR_PRIORITY, avatarPriority, getAvatarPriorityAsString());
     }
 
     // Web only
@@ -2123,6 +2149,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(skyboxMode, SkyboxMode);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(hazeMode, HazeMode);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(bloomMode, BloomMode);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(avatarPriority, AvatarPriority);
 
     // Polyvox
     COPY_PROPERTY_FROM_QSCRIPTVALUE(voxelVolumeSize, vec3, setVoxelVolumeSize);
@@ -2403,6 +2430,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
     COPY_PROPERTY_IF_CHANGED(skyboxMode);
     COPY_PROPERTY_IF_CHANGED(hazeMode);
     COPY_PROPERTY_IF_CHANGED(bloomMode);
+    COPY_PROPERTY_IF_CHANGED(avatarPriority);
 
     // Polyvox
     COPY_PROPERTY_IF_CHANGED(voxelVolumeSize);
@@ -2789,6 +2817,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         ADD_PROPERTY_TO_MAP(PROP_SKYBOX_MODE, SkyboxMode, skyboxMode, uint32_t);
         ADD_PROPERTY_TO_MAP(PROP_HAZE_MODE, HazeMode, hazeMode, uint32_t);
         ADD_PROPERTY_TO_MAP(PROP_BLOOM_MODE, BloomMode, bloomMode, uint32_t);
+        ADD_PROPERTY_TO_MAP(PROP_AVATAR_PRIORITY, AvatarPriority, avatarPriority, uint32_t);
 
         // Polyvox
         ADD_PROPERTY_TO_MAP(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, vec3);
@@ -3191,6 +3220,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, (uint32_t)properties.getAvatarPriority());
             }
 
             if (properties.getType() == EntityTypes::PolyVox) {
@@ -3656,6 +3686,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SKYBOX_MODE, uint32_t, setSkyboxMode);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_HAZE_MODE, uint32_t, setHazeMode);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_BLOOM_MODE, uint32_t, setBloomMode);
+        READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_AVATAR_PRIORITY, uint32_t, setAvatarPriority);
     }
 
     if (properties.getType() == EntityTypes::PolyVox) {
@@ -4039,6 +4070,7 @@ void EntityItemProperties::markAllChanged() {
     _skyboxModeChanged = true;
     _hazeModeChanged = true;
     _bloomModeChanged = true;
+    _avatarPriorityChanged = true;
 
     // Polyvox
     _voxelVolumeSizeChanged = true;
@@ -4637,6 +4669,9 @@ QList EntityItemProperties::listChangedProperties() {
     if (bloomModeChanged()) {
         out += "bloomMode";
     }
+    if (avatarPriorityChanged()) {
+        out += "avatarPriority";
+    }
 
     // Polyvox
     if (voxelVolumeSizeChanged()) {
diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h
index 00a93dd129..bc1784c93b 100644
--- a/libraries/entities/src/EntityItemProperties.h
+++ b/libraries/entities/src/EntityItemProperties.h
@@ -321,6 +321,7 @@ public:
     DEFINE_PROPERTY_REF_ENUM(PROP_AMBIENT_LIGHT_MODE, AmbientLightMode, ambientLightMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
     DEFINE_PROPERTY_REF_ENUM(PROP_HAZE_MODE, HazeMode, hazeMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
     DEFINE_PROPERTY_REF_ENUM(PROP_BLOOM_MODE, BloomMode, bloomMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
+    DEFINE_PROPERTY_REF_ENUM(PROP_AVATAR_PRIORITY, AvatarPriority, avatarPriority, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
 
     // Polyvox
     DEFINE_PROPERTY_REF(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3, PolyVoxEntityItem::DEFAULT_VOXEL_VOLUME_SIZE);
@@ -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..98b18869fc 100644
--- a/libraries/entities/src/ZoneEntityItem.cpp
+++ b/libraries/entities/src/ZoneEntityItem.cpp
@@ -71,6 +71,7 @@ EntityItemProperties ZoneEntityItem::getProperties(const EntityPropertyFlags& de
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(skyboxMode, getSkyboxMode);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(hazeMode, getHazeMode);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(bloomMode, getBloomMode);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(avatarPriority, getAvatarPriority);
 
     return properties;
 }
@@ -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, uint32_t, 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,18 @@ 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 set ignore only priority-inherit zones:
+    if (jsonFilters.contains(AVATAR_PRIORITY_PROPERTY) && jsonFilters[AVATAR_PRIORITY_PROPERTY].toBool()
+        && _avatarPriority != COMPONENT_MODE_INHERIT) {
+        return true;
+    }
+
+    // Chain to base:
+    return EntityItem::matchesJSONFilters(jsonFilters);
+}
diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h
index 11c85dab89..df6ce50fd6 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); 
 
+    uint32_t getAvatarPriority() const { return _avatarPriority; }
+    void setAvatarPriority(uint32_t value) { _avatarPriority = value; }
+
     bool keyLightPropertiesChanged() const { return _keyLightPropertiesChanged; }
     bool ambientLightPropertiesChanged() const { return _ambientLightPropertiesChanged; }
     bool skyboxPropertiesChanged() const { return _skyboxPropertiesChanged; }
@@ -147,6 +152,9 @@ protected:
     bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED };
     QString _filterURL { DEFAULT_FILTER_URL };
 
+    // Avatar-updates priority
+    uint32_t _avatarPriority { COMPONENT_MODE_INHERIT };
+
     // Dirty flags turn true when either keylight properties is changing values.
     bool _keyLightPropertiesChanged { false };
     bool _ambientLightPropertiesChanged { false };
diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp
index dfb18eef86..c0c473315d 100644
--- a/libraries/model-baker/src/model-baker/Baker.cpp
+++ b/libraries/model-baker/src/model-baker/Baker.cpp
@@ -117,7 +117,7 @@ namespace baker {
 
     class BakerEngineBuilder {
     public:
-        using Input = VaryingSet2;
+        using Input = VaryingSet2;
         using Output = VaryingSet2;
         using JobModel = Task::ModelIO;
         void build(JobModel& model, const Varying& input, Varying& output) {
@@ -169,7 +169,7 @@ namespace baker {
         }
     };
 
-    Baker::Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping) :
+    Baker::Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping) :
         _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) {
         _engine->feedInput(0, hfmModel);
         _engine->feedInput(1, mapping);
diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h
index 542be0b559..856b5f0142 100644
--- a/libraries/model-baker/src/model-baker/Baker.h
+++ b/libraries/model-baker/src/model-baker/Baker.h
@@ -17,13 +17,14 @@
 #include 
 
 #include "Engine.h"
+#include "BakerTypes.h"
 
 #include "ParseMaterialMappingTask.h"
 
 namespace baker {
     class Baker {
     public:
-        Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping);
+        Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping);
 
         void run();
 
diff --git a/libraries/model-baker/src/model-baker/BakerTypes.h b/libraries/model-baker/src/model-baker/BakerTypes.h
index 5d14ee5420..8b80b0bde4 100644
--- a/libraries/model-baker/src/model-baker/BakerTypes.h
+++ b/libraries/model-baker/src/model-baker/BakerTypes.h
@@ -12,6 +12,7 @@
 #ifndef hifi_BakerTypes_h
 #define hifi_BakerTypes_h
 
+#include 
 #include 
 
 namespace baker {
@@ -35,6 +36,7 @@ namespace baker {
     using TangentsPerBlendshape = std::vector>;
 
     using MeshIndicesToModelNames = QHash;
+    using GeometryMappingPair = std::pair;
 };
 
 #endif // hifi_BakerTypes_h
diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp
index 7a923a3702..0a1964d8cd 100644
--- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp
+++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp
@@ -10,7 +10,9 @@
 
 #include "ModelBakerLogging.h"
 
-void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& mapping, Output& output) {
+void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
+    const auto& url = input.first;
+    const auto& mapping = input.second;
     MaterialMapping materialMapping;
 
     auto mappingIter = mapping.find("materialMap");
@@ -59,14 +61,13 @@ void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, con
                     {
                         NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
                         materialResource->moveToThread(qApp->thread());
-                        // TODO: add baseURL to allow FSTs to reference relative files next to them
-                        materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), QUrl());
+                        materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url);
                         materialMapping.push_back(std::pair(mapping.toStdString(), materialResource));
                     }
 
                 } else if (mappingJSON.isString()) {
                     auto mappingValue = mappingJSON.toString();
-                    materialMapping.push_back(std::pair(mapping.toStdString(), MaterialCache::instance().getMaterial(mappingValue)));
+                    materialMapping.push_back(std::pair(mapping.toStdString(), MaterialCache::instance().getMaterial(url.resolved(mappingValue))));
                 }
             }
         }
diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h
index 69e00b0324..5f5eff327d 100644
--- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h
+++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h
@@ -14,12 +14,13 @@
 #include 
 
 #include "Engine.h"
+#include "BakerTypes.h"
 
 #include 
 
 class ParseMaterialMappingTask {
 public:
-    using Input = QVariantHash;
+    using Input = baker::GeometryMappingPair;
     using Output = MaterialMapping;
     using JobModel = baker::Job::ModelIO;
 
diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp
index 3b1a57cb43..a896766058 100644
--- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp
+++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp
@@ -58,7 +58,7 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu
     auto& jointIndices = output.edit2();
 
     // Get joint renames
-    auto jointNameMapping = getJointNameMapping(mapping);
+    auto jointNameMapping = getJointNameMapping(mapping.second);
     // Apply joint metadata from FST file mappings
     for (const auto& jointIn : jointsIn) {
         jointsOut.push_back(jointIn);
@@ -73,7 +73,7 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu
     }
 
     // Get joint rotation offsets from FST file mappings
-    auto offsets = getJointRotationOffsets(mapping);
+    auto offsets = getJointRotationOffsets(mapping.second);
     for (auto itr = offsets.begin(); itr != offsets.end(); itr++) {
         QString jointName = itr.key();
         int jointIndex = jointIndices.value(jointName) - 1;
diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h
index e12d8ffd2c..b18acdfceb 100644
--- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h
+++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h
@@ -17,10 +17,11 @@
 #include 
 
 #include "Engine.h"
+#include "BakerTypes.h"
 
 class PrepareJointsTask {
 public:
-    using Input = baker::VaryingSet2, QVariantHash /*mapping*/>;
+    using Input = baker::VaryingSet2, baker::GeometryMappingPair /*mapping*/>;
     using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>;
     using JobModel = baker::Job::ModelIO;
 
diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp
index 581196b2cc..a48f96eb1b 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.cpp
+++ b/libraries/model-networking/src/model-networking/ModelCache.cpp
@@ -35,11 +35,13 @@ class GeometryReader;
 
 class GeometryExtra {
 public:
-    const QVariantHash& mapping;
+    const GeometryMappingPair& mapping;
     const QUrl& textureBaseUrl;
     bool combineParts;
 };
 
+int geometryMappingPairTypeId = qRegisterMetaType("GeometryMappingPair");
+
 // From: https://stackoverflow.com/questions/41145012/how-to-hash-qvariant
 class QVariantHasher {
 public:
@@ -78,7 +80,8 @@ namespace std {
     struct hash {
         size_t operator()(const GeometryExtra& geometryExtra) const {
             size_t result = 0;
-            hash_combine(result, geometryExtra.mapping, geometryExtra.textureBaseUrl, geometryExtra.combineParts);
+            hash_combine(result, geometryExtra.mapping.first, geometryExtra.mapping.second, geometryExtra.textureBaseUrl,
+                geometryExtra.combineParts);
             return result;
         }
     };
@@ -151,7 +154,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) {
         }
 
         auto modelCache = DependencyManager::get();
-        GeometryExtra extra { _mapping, _textureBaseUrl, false };
+        GeometryExtra extra { GeometryMappingPair(_url, _mapping), _textureBaseUrl, false };
 
         // Get the raw GeometryResource
         _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast();
@@ -191,7 +194,7 @@ void GeometryMappingResource::onGeometryMappingLoaded(bool success) {
 
 class GeometryReader : public QRunnable {
 public:
-    GeometryReader(const ModelLoader& modelLoader, QWeakPointer& resource, const QUrl& url, const QVariantHash& mapping,
+    GeometryReader(const ModelLoader& modelLoader, QWeakPointer& resource, const QUrl& url, const GeometryMappingPair& mapping,
                    const QByteArray& data, bool combineParts, const QString& webMediaType) :
         _modelLoader(modelLoader), _resource(resource), _url(url), _mapping(mapping), _data(data), _combineParts(combineParts), _webMediaType(webMediaType) {
 
@@ -204,7 +207,7 @@ private:
     ModelLoader _modelLoader;
     QWeakPointer _resource;
     QUrl _url;
-    QVariantHash _mapping;
+    GeometryMappingPair _mapping;
     QByteArray _data;
     bool _combineParts;
     QString _webMediaType;
@@ -244,7 +247,7 @@ void GeometryReader::run() {
         }
 
         HFMModel::Pointer hfmModel;
-        QVariantHash serializerMapping = _mapping;
+        QVariantHash serializerMapping = _mapping.second;
         serializerMapping["combineParts"] = _combineParts;
 
         if (_url.path().toLower().endsWith(".gz")) {
@@ -270,15 +273,14 @@ void GeometryReader::run() {
         }
 
         // Add scripts to hfmModel
-        if (!_mapping.value(SCRIPT_FIELD).isNull()) {
-            QVariantList scripts = _mapping.values(SCRIPT_FIELD);
+        if (!serializerMapping.value(SCRIPT_FIELD).isNull()) {
+            QVariantList scripts = serializerMapping.values(SCRIPT_FIELD);
             for (auto &script : scripts) {
                 hfmModel->scripts.push_back(script.toString());
             }
         }
-
         QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition",
-                Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(QVariantHash, _mapping));
+                Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(GeometryMappingPair, _mapping));
     } catch (const std::exception&) {
         auto resource = _resource.toStrongRef();
         if (resource) {
@@ -312,17 +314,17 @@ public:
     void setExtra(void* extra) override;
 
 protected:
-    Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping);
+    Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping);
 
 private:
     ModelLoader _modelLoader;
-    QVariantHash _mapping;
+    GeometryMappingPair _mapping;
     bool _combineParts;
 };
 
 void GeometryDefinitionResource::setExtra(void* extra) {
     const GeometryExtra* geometryExtra = static_cast(extra);
-    _mapping = geometryExtra ? geometryExtra->mapping : QVariantHash();
+    _mapping = geometryExtra ? geometryExtra->mapping : GeometryMappingPair(QUrl(), QVariantHash());
     _textureBaseUrl = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl();
     _combineParts = geometryExtra ? geometryExtra->combineParts : true;
 }
@@ -335,7 +337,7 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) {
     QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mapping, data, _combineParts, _request->getWebMediaType()));
 }
 
-void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping) {
+void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) {
     // Do processing on the model
     baker::Baker modelBaker(hfmModel, mapping);
     modelBaker.run();
@@ -394,11 +396,15 @@ QSharedPointer ModelCache::createResource(const QUrl& url) {
 }
 
 QSharedPointer ModelCache::createResourceCopy(const QSharedPointer& resource) {
-    return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter);
+    if (resource->getURL().path().toLower().endsWith(".fst")) {
+        return QSharedPointer(new GeometryMappingResource(*resource.staticCast()), &Resource::deleter);
+    } else {
+        return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter);
+    }
 }
 
 GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url,
-                                                          const QVariantHash& mapping, const QUrl& textureBaseUrl) {
+                                                          const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) {
     bool combineParts = true;
     GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
     GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast();
@@ -411,7 +417,8 @@ GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url,
 }
 
 GeometryResource::Pointer ModelCache::getCollisionGeometryResource(const QUrl& url,
-                                                                   const QVariantHash& mapping, const QUrl& textureBaseUrl) {
+                                                                   const GeometryMappingPair& mapping,
+                                                                   const QUrl& textureBaseUrl) {
     bool combineParts = false;
     GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
     GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast();
diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h
index 4cd7048dca..ca1ceaff16 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.h
+++ b/libraries/model-networking/src/model-networking/ModelCache.h
@@ -26,6 +26,9 @@ class MeshPart;
 
 class GeometryMappingResource;
 
+using GeometryMappingPair = std::pair;
+Q_DECLARE_METATYPE(GeometryMappingPair)
+
 class Geometry {
 public:
     using Pointer = std::shared_ptr;
@@ -145,11 +148,13 @@ class ModelCache : public ResourceCache, public Dependency {
 public:
 
     GeometryResource::Pointer getGeometryResource(const QUrl& url,
-                                                  const QVariantHash& mapping = QVariantHash(),
+                                                  const GeometryMappingPair& mapping =
+                                                        GeometryMappingPair(QUrl(), QVariantHash()),
                                                   const QUrl& textureBaseUrl = QUrl());
 
     GeometryResource::Pointer getCollisionGeometryResource(const QUrl& url,
-                                                           const QVariantHash& mapping = QVariantHash(),
+                                                           const GeometryMappingPair& mapping =
+                                                                 GeometryMappingPair(QUrl(), QVariantHash()),
                                                            const QUrl& textureBaseUrl = QUrl());
 
 protected:
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index eaa02f059e..a9dbc12b09 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -40,6 +40,9 @@
 
 static Setting::Handle LIMITED_NODELIST_LOCAL_PORT("LimitedNodeList.LocalPort", 0);
 
+using namespace std::chrono_literals;
+static const std::chrono::milliseconds CONNECTION_RATE_INTERVAL_MS = 1s;
+
 const std::set SOLO_NODE_TYPES = {
     NodeType::AvatarMixer,
     NodeType::AudioMixer,
@@ -88,6 +91,11 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) :
     connect(statsSampleTimer, &QTimer::timeout, this, &LimitedNodeList::sampleConnectionStats);
     statsSampleTimer->start(CONNECTION_STATS_SAMPLE_INTERVAL_MSECS);
 
+    // Flush delayed adds every second
+    QTimer* delayedAddsFlushTimer = new QTimer(this);
+    connect(delayedAddsFlushTimer, &QTimer::timeout, this, &NodeList::processDelayedAdds);
+    delayedAddsFlushTimer->start(CONNECTION_RATE_INTERVAL_MS.count());
+
     // check the local socket right now
     updateLocalSocket();
 
@@ -367,7 +375,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
 
             return true;
 
-        } else {
+        } else if (!isDelayedNode(sourceID)){
             HIFI_FCDEBUG(networking(),
                 "Packet of type" << headerType << "received from unknown node with Local ID" << sourceLocalID);
         }
@@ -558,25 +566,23 @@ SharedNodePointer LimitedNodeList::nodeWithLocalID(Node::LocalID localID) const
 }
 
 void LimitedNodeList::eraseAllNodes() {
-    QSet killedNodes;
+    std::vector killedNodes;
 
     {
         // iterate the current nodes - grab them so we can emit that they are dying
         // and then remove them from the hash
         QWriteLocker writeLocker(&_nodeMutex);
 
-        _localIDMap.clear();
-
         if (_nodeHash.size() > 0) {
             qCDebug(networking) << "LimitedNodeList::eraseAllNodes() removing all nodes from NodeList.";
 
-            auto it = _nodeHash.begin();
-
-            while (it != _nodeHash.end())  {
-                killedNodes.insert(it->second);
-                it = _nodeHash.unsafe_erase(it);
+            killedNodes.reserve(_nodeHash.size());
+            for (auto& pair : _nodeHash) {
+                killedNodes.push_back(pair.second);
             }
         }
+        _localIDMap.clear();
+        _nodeHash.clear();
     }
 
     foreach(const SharedNodePointer& killedNode, killedNodes) {
@@ -593,18 +599,13 @@ void LimitedNodeList::reset() {
 }
 
 bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID, ConnectionID newConnectionID) {
-    QReadLocker readLocker(&_nodeMutex);
-
-    NodeHash::iterator it = _nodeHash.find(nodeUUID);
-    if (it != _nodeHash.end()) {
-        SharedNodePointer matchingNode = it->second;
-
-        readLocker.unlock();
+    auto matchingNode = nodeWithUUID(nodeUUID);
 
+    if (matchingNode) {
         {
             QWriteLocker writeLocker(&_nodeMutex);
             _localIDMap.unsafe_erase(matchingNode->getLocalID());
-            _nodeHash.unsafe_erase(it);
+            _nodeHash.unsafe_erase(matchingNode->getUUID());
         }
 
         handleNodeKill(matchingNode, newConnectionID);
@@ -645,30 +646,26 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
                                                    const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
                                                    Node::LocalID localID, bool isReplicated, bool isUpstream,
                                                    const QUuid& connectionSecret, const NodePermissions& permissions) {
-    {
-        QReadLocker readLocker(&_nodeMutex);
-        NodeHash::const_iterator it = _nodeHash.find(uuid);
+    auto matchingNode = nodeWithUUID(uuid);
+    if (matchingNode) {
+        matchingNode->setPublicSocket(publicSocket);
+        matchingNode->setLocalSocket(localSocket);
+        matchingNode->setPermissions(permissions);
+        matchingNode->setConnectionSecret(connectionSecret);
+        matchingNode->setIsReplicated(isReplicated);
+        matchingNode->setIsUpstream(isUpstream || NodeType::isUpstream(nodeType));
+        matchingNode->setLocalID(localID);
 
-        if (it != _nodeHash.end()) {
-            SharedNodePointer& matchingNode = it->second;
-
-            matchingNode->setPublicSocket(publicSocket);
-            matchingNode->setLocalSocket(localSocket);
-            matchingNode->setPermissions(permissions);
-            matchingNode->setConnectionSecret(connectionSecret);
-            matchingNode->setIsReplicated(isReplicated);
-            matchingNode->setIsUpstream(isUpstream || NodeType::isUpstream(nodeType));
-            matchingNode->setLocalID(localID);
-
-            return matchingNode;
-        }
+        return matchingNode;
     }
 
     auto removeOldNode = [&](auto node) {
         if (node) {
-            QWriteLocker writeLocker(&_nodeMutex);
-            _localIDMap.unsafe_erase(node->getLocalID());
-            _nodeHash.unsafe_erase(node->getUUID());
+            {
+                QWriteLocker writeLocker(&_nodeMutex);
+                _localIDMap.unsafe_erase(node->getLocalID());
+                _nodeHash.unsafe_erase(node->getUUID());
+            }
             handleNodeKill(node);
         }
     };
@@ -736,6 +733,53 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
     return newNodePointer;
 }
 
+void LimitedNodeList::addNewNode(NewNodeInfo info) {
+    // Throttle connection of new agents.
+    if (info.type == NodeType::Agent && _nodesAddedInCurrentTimeSlice >= _maxConnectionRate) {
+        delayNodeAdd(info);
+        return;
+    }
+
+    SharedNodePointer node = addOrUpdateNode(info.uuid, info.type, info.publicSocket, info.localSocket,
+                                             info.sessionLocalID, info.isReplicated, false,
+                                             info.connectionSecretUUID, info.permissions);
+
+    ++_nodesAddedInCurrentTimeSlice;
+}
+
+void LimitedNodeList::delayNodeAdd(NewNodeInfo info) {
+    _delayedNodeAdds.push_back(info);
+}
+
+void LimitedNodeList::removeDelayedAdd(QUuid nodeUUID) {
+    auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](auto info) {
+        return info.uuid == nodeUUID;
+    });
+    if (it != _delayedNodeAdds.end()) {
+        _delayedNodeAdds.erase(it);
+    }
+}
+
+bool LimitedNodeList::isDelayedNode(QUuid nodeUUID) {
+    auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](auto info) {
+        return info.uuid == nodeUUID;
+    });
+    return it != _delayedNodeAdds.end();
+}
+
+void LimitedNodeList::processDelayedAdds() {
+    _nodesAddedInCurrentTimeSlice = 0;
+
+    auto nodesToAdd = glm::min(_delayedNodeAdds.size(), _maxConnectionRate);
+    auto firstNodeToAdd = _delayedNodeAdds.begin();
+    auto lastNodeToAdd = firstNodeToAdd + nodesToAdd;
+
+    for (auto it = firstNodeToAdd; it != lastNodeToAdd; ++it) {
+        addNewNode(*it);
+    }
+    _delayedNodeAdds.erase(firstNodeToAdd, lastNodeToAdd);
+}
+
 std::unique_ptr LimitedNodeList::constructPingPacket(const QUuid& nodeId, PingType_t pingType) {
     int packetSize = sizeof(PingType_t) + sizeof(quint64) + sizeof(int64_t);
 
@@ -793,13 +837,13 @@ unsigned int LimitedNodeList::broadcastToNodes(std::unique_ptr packet,
 
     eachNode([&](const SharedNodePointer& node){
         if (node && destinationNodeTypes.contains(node->getType())) {
-			if (packet->isReliable()) {
-				auto packetCopy = NLPacket::createCopy(*packet);
-				sendPacket(std::move(packetCopy), *node);
-			} else {
-				sendUnreliablePacket(*packet, *node);
-			}
-			++n;
+            if (packet->isReliable()) {
+                auto packetCopy = NLPacket::createCopy(*packet);
+                sendPacket(std::move(packetCopy), *node);
+            } else {
+                sendUnreliablePacket(*packet, *node);
+            }
+            ++n;
         }
     });
 
diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h
index 450fad96a9..8593ad4b1b 100644
--- a/libraries/networking/src/LimitedNodeList.h
+++ b/libraries/networking/src/LimitedNodeList.h
@@ -51,6 +51,8 @@ const int INVALID_PORT = -1;
 
 const quint64 NODE_SILENCE_THRESHOLD_MSECS = 5 * 1000;
 
+static const size_t DEFAULT_MAX_CONNECTION_RATE { std::numeric_limits::max() };
+
 extern const std::set SOLO_NODE_TYPES;
 
 const char DEFAULT_ASSIGNMENT_SERVER_HOSTNAME[] = "localhost";
@@ -205,7 +207,10 @@ public:
                     int* lockWaitOut = nullptr,
                     int* nodeTransformOut = nullptr,
                     int* functorOut = nullptr) {
-        auto start = usecTimestampNow();
+        quint64 start, endTransform, endFunctor;
+
+        start = usecTimestampNow();
+        std::vector nodes;
         {
             QReadLocker readLock(&_nodeMutex);
             auto endLock = usecTimestampNow();
@@ -216,21 +221,21 @@ public:
             // Size of _nodeHash could change at any time,
             // so reserve enough memory for the current size
             // and then back insert all the nodes found
-            std::vector nodes;
             nodes.reserve(_nodeHash.size());
             std::transform(_nodeHash.cbegin(), _nodeHash.cend(), std::back_inserter(nodes), [&](const NodeHash::value_type& it) {
                 return it.second;
             });
-            auto endTransform = usecTimestampNow();
+
+            endTransform = usecTimestampNow();
             if (nodeTransformOut) {
                 *nodeTransformOut = (endTransform - endLock);
             }
+        }
 
-            functor(nodes.cbegin(), nodes.cend());
-            auto endFunctor = usecTimestampNow();
-            if (functorOut) {
-                *functorOut = (endFunctor - endTransform);
-            }
+        functor(nodes.cbegin(), nodes.cend());
+        endFunctor = usecTimestampNow();
+        if (functorOut) {
+            *functorOut = (endFunctor - endTransform);
         }
     }
 
@@ -316,6 +321,9 @@ public:
     void sendFakedHandshakeRequestToNode(SharedNodePointer node);
 #endif
 
+    size_t getMaxConnectionRate() const { return _maxConnectionRate; }
+    void setMaxConnectionRate(size_t rate) { _maxConnectionRate = rate; }
+
     int getInboundPPS() const { return _inboundPPS; }
     int getOutboundPPS() const { return _outboundPPS; }
     float getInboundKbps() const { return _inboundKbps; }
@@ -367,7 +375,20 @@ protected slots:
 
     void clientConnectionToSockAddrReset(const HifiSockAddr& sockAddr);
 
+    void processDelayedAdds();
+
 protected:
+    struct NewNodeInfo {
+        qint8 type;
+        QUuid uuid;
+        HifiSockAddr publicSocket;
+        HifiSockAddr localSocket;
+        NodePermissions permissions;
+        bool isReplicated;
+        Node::LocalID sessionLocalID;
+        QUuid connectionSecretUUID;
+    };
+
     LimitedNodeList(int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT);
     LimitedNodeList(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton
     void operator=(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton
@@ -390,6 +411,11 @@ protected:
 
     bool sockAddrBelongsToNode(const HifiSockAddr& sockAddr);
 
+    void addNewNode(NewNodeInfo info);
+    void delayNodeAdd(NewNodeInfo info);
+    void removeDelayedAdd(QUuid nodeUUID);
+    bool isDelayedNode(QUuid nodeUUID);
+
     NodeHash _nodeHash;
     mutable QReadWriteLock _nodeMutex { QReadWriteLock::Recursive };
     udt::Socket _nodeSocket;
@@ -440,6 +466,10 @@ private:
     Node::LocalID _sessionLocalID { 0 };
     bool _flagTimeForConnectionStep { false }; // only keep track in interface
 
+    size_t _maxConnectionRate { DEFAULT_MAX_CONNECTION_RATE };
+    size_t _nodesAddedInCurrentTimeSlice { 0 };
+    std::vector _delayedNodeAdds;
+
     int _inboundPPS { 0 };
     int _outboundPPS { 0 };
     float _inboundKbps { 0.0f };
diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp
index 5e8909db2b..e6eb6087b0 100644
--- a/libraries/networking/src/NodeList.cpp
+++ b/libraries/networking/src/NodeList.cpp
@@ -200,7 +200,6 @@ void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer&
 }
 
 void NodeList::processPingPacket(QSharedPointer message, SharedNodePointer sendingNode) {
-
     // send back a reply
     auto replyPacket = constructPingReplyPacket(*message);
     const HifiSockAddr& senderSockAddr = message->getSenderSockAddr();
@@ -291,41 +290,47 @@ void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes)
 
 void NodeList::sendDomainServerCheckIn() {
 
+    // This function is called by the server check-in timer thread
+    // not the NodeList thread.  Calling it on the NodeList thread
+    // resulted in starvation of the server check-in function.
+    // be VERY CAREFUL modifying this code as members of NodeList
+    // may be called by multiple threads.
+
     if (!_sendDomainServerCheckInEnabled) {
         qCDebug(networking) << "Refusing to send a domain-server check in while it is disabled.";
         return;
     }
 
-    if (thread() != QThread::currentThread()) {
-        QMetaObject::invokeMethod(this, "sendDomainServerCheckIn", Qt::QueuedConnection);
-        return;
-    }
-
     if (_isShuttingDown) {
         qCDebug(networking) << "Refusing to send a domain-server check in while shutting down.";
         return;
     }
 
-    if (_publicSockAddr.isNull()) {
+    auto publicSockAddr = _publicSockAddr;
+    auto domainHandlerIp = _domainHandler.getIP();
+
+    if (publicSockAddr.isNull()) {
         // we don't know our public socket and we need to send it to the domain server
         qCDebug(networking) << "Waiting for inital public socket from STUN. Will not send domain-server check in.";
-    } else if (_domainHandler.getIP().isNull() && _domainHandler.requiresICE()) {
+    } else if (domainHandlerIp.isNull() && _domainHandler.requiresICE()) {
         qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in.";
         handleICEConnectionToDomainServer();
         // let the domain handler know we are due to send a checkin packet
-    } else if (!_domainHandler.getIP().isNull() && !_domainHandler.checkInPacketTimeout()) {
-
-        PacketType domainPacketType = !_domainHandler.isConnected()
+    } else if (!domainHandlerIp.isNull() && !_domainHandler.checkInPacketTimeout()) {
+        bool domainIsConnected = _domainHandler.isConnected();
+        HifiSockAddr domainSockAddr = _domainHandler.getSockAddr();
+        PacketType domainPacketType = !domainIsConnected
             ? PacketType::DomainConnectRequest : PacketType::DomainListRequest;
 
-        if (!_domainHandler.isConnected()) {
-            qCDebug(networking) << "Sending connect request to domain-server at" << _domainHandler.getHostname();
+        if (!domainIsConnected) {
+            auto hostname = _domainHandler.getHostname();
+            qCDebug(networking) << "Sending connect request to domain-server at" << hostname;
 
             // is this our localhost domain-server?
             // if so we need to make sure we have an up-to-date local port in case it restarted
 
-            if (_domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost
-                || _domainHandler.getHostname() == "localhost") {
+            if (domainSockAddr.getAddress() == QHostAddress::LocalHost
+                || hostname == "localhost") {
 
                 quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT;
                 getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, domainPort);
@@ -338,7 +343,7 @@ void NodeList::sendDomainServerCheckIn() {
         auto accountManager = DependencyManager::get();
         const QUuid& connectionToken = _domainHandler.getConnectionToken();
 
-        bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull();
+        bool requiresUsernameSignature = !domainIsConnected && !connectionToken.isNull();
 
         if (requiresUsernameSignature && !accountManager->getAccountInfo().hasPrivateKey()) {
             qWarning() << "A keypair is required to present a username signature to the domain-server"
@@ -353,6 +358,7 @@ void NodeList::sendDomainServerCheckIn() {
 
         QDataStream packetStream(domainPacket.get());
 
+        HifiSockAddr localSockAddr = _localSockAddr;
         if (domainPacketType == PacketType::DomainConnectRequest) {
 
 #if (PR_BUILD || DEV_BUILD)
@@ -361,13 +367,9 @@ void NodeList::sendDomainServerCheckIn() {
             }
 #endif
 
-            QUuid connectUUID;
+            QUuid connectUUID = _domainHandler.getAssignmentUUID();
 
-            if (!_domainHandler.getAssignmentUUID().isNull()) {
-                // this is a connect request and we're an assigned node
-                // so set our packetUUID as the assignment UUID
-                connectUUID = _domainHandler.getAssignmentUUID();
-            } else if (_domainHandler.requiresICE()) {
+            if (connectUUID.isNull() && _domainHandler.requiresICE()) {
                 // this is a connect request and we're an interface client
                 // that used ice to discover the DS
                 // so send our ICE client UUID with the connect request
@@ -383,10 +385,9 @@ void NodeList::sendDomainServerCheckIn() {
 
             // if possible, include the MAC address for the current interface in our connect request
             QString hardwareAddress;
-
             for (auto networkInterface : QNetworkInterface::allInterfaces()) {
                 for (auto interfaceAddress : networkInterface.addressEntries()) {
-                    if (interfaceAddress.ip() == _localSockAddr.getAddress()) {
+                    if (interfaceAddress.ip() == localSockAddr.getAddress()) {
                         // this is the interface whose local IP matches what we've detected the current IP to be
                         hardwareAddress = networkInterface.hardwareAddress();
 
@@ -410,10 +411,10 @@ void NodeList::sendDomainServerCheckIn() {
 
         // pack our data to send to the domain-server including
         // the hostname information (so the domain-server can see which place name we came in on)
-        packetStream << _ownerType.load() << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
+        packetStream << _ownerType.load() << publicSockAddr << localSockAddr << _nodeTypesOfInterest.toList();
         packetStream << DependencyManager::get()->getPlaceName();
 
-        if (!_domainHandler.isConnected()) {
+        if (!domainIsConnected) {
             DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
             packetStream << accountInfo.getUsername();
 
@@ -433,9 +434,9 @@ void NodeList::sendDomainServerCheckIn() {
         checkinCount = std::min(checkinCount, MAX_CHECKINS_TOGETHER);
         for (int i = 1; i < checkinCount; ++i) {
             auto packetCopy = domainPacket->createCopy(*domainPacket);
-            sendPacket(std::move(packetCopy), _domainHandler.getSockAddr());
+            sendPacket(std::move(packetCopy), domainSockAddr);
         }
-        sendPacket(std::move(domainPacket), _domainHandler.getSockAddr());
+        sendPacket(std::move(domainPacket), domainSockAddr);
         
     }
 }
@@ -708,37 +709,28 @@ void NodeList::processDomainServerRemovedNode(QSharedPointer me
     QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
     qCDebug(networking) << "Received packet from domain-server to remove node with UUID" << uuidStringWithoutCurlyBraces(nodeUUID);
     killNodeWithUUID(nodeUUID);
+    removeDelayedAdd(nodeUUID);
 }
 
 void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
-    // setup variables to read into from QDataStream
-    qint8 nodeType;
-    QUuid nodeUUID, connectionSecretUUID;
-    HifiSockAddr nodePublicSocket, nodeLocalSocket;
-    NodePermissions permissions;
-    bool isReplicated;
-    Node::LocalID sessionLocalID;
+    NewNodeInfo info;
 
-    packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions
-        >> isReplicated >> sessionLocalID;
+    packetStream >> info.type
+                 >> info.uuid
+                 >> info.publicSocket
+                 >> info.localSocket
+                 >> info.permissions
+                 >> info.isReplicated
+                 >> info.sessionLocalID
+                 >> info.connectionSecretUUID;
 
     // if the public socket address is 0 then it's reachable at the same IP
     // as the domain server
-    if (nodePublicSocket.getAddress().isNull()) {
-        nodePublicSocket.setAddress(_domainHandler.getIP());
+    if (info.publicSocket.getAddress().isNull()) {
+        info.publicSocket.setAddress(_domainHandler.getIP());
     }
 
-    packetStream >> connectionSecretUUID;
-
-    SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket, nodeLocalSocket,
-                                             sessionLocalID, isReplicated, false, connectionSecretUUID, permissions);
-
-    // nodes that are downstream or upstream of our own type are kept alive when we hear about them from the domain server
-    // and always have their public socket as their active socket
-    if (node->getType() == NodeType::downstreamType(_ownerType) || node->getType() == NodeType::upstreamType(_ownerType)) {
-        node->setLastHeardMicrostamp(usecTimestampNow());
-        node->activatePublicSocket();
-    }
+    addNewNode(info);
 }
 
 void NodeList::sendAssignment(Assignment& assignment) {
@@ -785,7 +777,6 @@ void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) {
 }
 
 void NodeList::startNodeHolePunch(const SharedNodePointer& node) {
-
     // we don't hole punch to downstream servers, since it is assumed that we have a direct line to them
     // we also don't hole punch to relayed upstream nodes, since we do not communicate directly with them
 
@@ -799,6 +790,14 @@ void NodeList::startNodeHolePunch(const SharedNodePointer& node) {
         // ping this node immediately
         pingPunchForInactiveNode(node);
     }
+
+    // nodes that are downstream or upstream of our own type are kept alive when we hear about them from the domain server
+    // and always have their public socket as their active socket
+    if (node->getType() == NodeType::downstreamType(_ownerType) || node->getType() == NodeType::upstreamType(_ownerType)) {
+        node->setLastHeardMicrostamp(usecTimestampNow());
+        node->activatePublicSocket();
+    }
+
 }
 
 void NodeList::handleNodePingTimeout() {
diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp
index bdba47f0ed..9b9a53b469 100644
--- a/libraries/networking/src/ThreadedAssignment.cpp
+++ b/libraries/networking/src/ThreadedAssignment.cpp
@@ -102,6 +102,11 @@ void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObjec
 
     statsObject["io_stats"] = ioStats;
 
+    QJsonObject assignmentStats;
+    assignmentStats["numQueuedCheckIns"] = _numQueuedCheckIns;
+
+    statsObject["assignmentStats"] = assignmentStats;
+
     nodeList->sendStatsToDomainServer(statsObject);
 }
 
@@ -119,10 +124,16 @@ void ThreadedAssignment::checkInWithDomainServerOrExit() {
         stop();
     } else {
         auto nodeList = DependencyManager::get();
-        QMetaObject::invokeMethod(nodeList.data(), "sendDomainServerCheckIn");
+        // Call sendDomainServerCheckIn directly instead of putting it on
+        // the event queue.  Under high load, the event queue can back up
+        // longer than the total timeout period and cause a restart
+        nodeList->sendDomainServerCheckIn();
 
         // 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..7201cdecad 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":  "Alter Avatars' update priorities."
+    },
     "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..2c3785217c 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: "inherit"
     },
     Model: {
         collisionShape: "none",
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js
index c1a8f363b5..a5e6a88656 100644
--- a/scripts/system/html/js/entityProperties.js
+++ b/scripts/system/html/js/entityProperties.js
@@ -428,6 +428,13 @@ const GROUPS = [
                 propertyID: "bloom.bloomSize",
                 showPropertyRule: { "bloomMode": "enabled" },
             },
+            {
+                label: "Avatar Priority",
+                type: "dropdown",
+                options: { inherit: "Inherit", crowd: "Crowd", hero: "Hero" },
+                propertyID: "avatarPriority",
+            },
+
         ]
     },
     {
diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js
index 0c4412abfb..7369cf91f8 100644
--- a/scripts/system/modules/createWindow.js
+++ b/scripts/system/modules/createWindow.js
@@ -125,9 +125,6 @@ module.exports = (function() {
 
             Script.scriptEnding.connect(this, function() {
                 this.window.close();
-                // FIXME: temp solution for reload crash (MS18269),
-                // we should decide on proper object ownership strategy for InteractiveWindow API
-                this.window = null;
             });
         },
         setVisible: function(visible) {
diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp
index 4e83460b9e..6dcc255286 100644
--- a/tools/nitpick/src/AWSInterface.cpp
+++ b/tools/nitpick/src/AWSInterface.cpp
@@ -27,7 +27,10 @@ AWSInterface::AWSInterface(QObject* parent) : QObject(parent) {
 void AWSInterface::createWebPageFromResults(const QString& testResults,
                                             const QString& workingDirectory,
                                             QCheckBox* updateAWSCheckBox,
-                                            QLineEdit* urlLineEdit) {
+                                            QRadioButton* diffImageRadioButton,
+                                            QRadioButton* ssimImageRadionButton,
+                                            QLineEdit* urlLineEdit
+) {
     _workingDirectory = workingDirectory;
 
     // Verify filename is in correct format 
@@ -52,6 +55,13 @@ void AWSInterface::createWebPageFromResults(const QString& testResults,
 
     QString zipFilenameWithoutExtension = zipFilename.split('.')[0];
     extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension);
+
+    if (diffImageRadioButton->isChecked()) {
+        _comparisonImageFilename = "Difference Image.png";
+    } else {
+        _comparisonImageFilename = "SSIM Image.png";
+    }
+        
     createHTMLFile();
 
     if (updateAWSCheckBox->isChecked()) {
@@ -353,7 +363,7 @@ void AWSInterface::openTable(QTextStream& stream, const QString& testResult, con
         stream << "\t\t\t\t

Test

\n"; stream << "\t\t\t\t

Actual Image

\n"; stream << "\t\t\t\t

Expected Image

\n"; - stream << "\t\t\t\t

Difference Image

\n"; + stream << "\t\t\t\t

Comparison Image

\n"; stream << "\t\t\t\n"; } } @@ -378,12 +388,13 @@ void AWSInterface::createEntry(const int index, const QString& testResult, QText QString folder; bool differenceFileFound; + if (isFailure) { folder = FAILURES_FOLDER; - differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/Difference Image.png"); + differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/" + _comparisonImageFilename); } else { folder = SUCCESSES_FOLDER; - differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/Difference Image.png"); + differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/" + _comparisonImageFilename); } if (textResultsFileFound) { @@ -450,7 +461,7 @@ void AWSInterface::createEntry(const int index, const QString& testResult, QText stream << "\t\t\t\t\n"; if (differenceFileFound) { - stream << "\t\t\t\t\n"; + stream << "\t\t\t\t\n"; } else { stream << "\t\t\t\t

No Image Found

\n"; } @@ -512,12 +523,12 @@ void AWSInterface::updateAWS() { stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; - if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { + if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/" + _comparisonImageFilename)) { stream << "data = open('" << _workingDirectory << "/" << filename << "/" - << "Difference Image.png" + << _comparisonImageFilename << "', 'rb')\n"; - stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << _comparisonImageFilename << "', Body=data)\n\n"; } } } @@ -555,12 +566,12 @@ void AWSInterface::updateAWS() { stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; - if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { + if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/" + _comparisonImageFilename)) { stream << "data = open('" << _workingDirectory << "/" << filename << "/" - << "Difference Image.png" + << _comparisonImageFilename << "', 'rb')\n"; - stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << _comparisonImageFilename << "', Body=data)\n\n"; } } } diff --git a/tools/nitpick/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h index d95b8ecf2f..77d500fa7c 100644 --- a/tools/nitpick/src/AWSInterface.h +++ b/tools/nitpick/src/AWSInterface.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "BusyWindow.h" @@ -28,6 +29,8 @@ public: void createWebPageFromResults(const QString& testResults, const QString& workingDirectory, QCheckBox* updateAWSCheckBox, + QRadioButton* diffImageRadioButton, + QRadioButton* ssimImageRadionButton, QLineEdit* urlLineEdit); void extractTestFailuresFromZippedFolder(const QString& folderName); @@ -67,6 +70,9 @@ private: QString AWS_BUCKET{ "hifi-qa" }; QLineEdit* _urlLineEdit; + + + QString _comparisonImageFilename; }; #endif // hifi_AWSInterface_h \ No newline at end of file diff --git a/tools/nitpick/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp index fa73f97887..7e3e6eaf63 100644 --- a/tools/nitpick/src/ImageComparer.cpp +++ b/tools/nitpick/src/ImageComparer.cpp @@ -14,7 +14,7 @@ // Computes SSIM - see https://en.wikipedia.org/wiki/Structural_similarity // The value is computed for the luminance component and the average value is returned -double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) const { +void ImageComparer::compareImages(const QImage& resultImage, const QImage& expectedImage) { const int L = 255; // (2^number of bits per pixel) - 1 const double K1 { 0.01 }; @@ -39,8 +39,13 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co double p[WIN_SIZE * WIN_SIZE]; double q[WIN_SIZE * WIN_SIZE]; + _ssimResults.results.clear(); + int windowCounter{ 0 }; double ssim{ 0.0 }; + double min { 1.0 }; + double max { -1.0 }; + while (x < expectedImage.width()) { int lastX = x + WIN_SIZE - 1; if (lastX > expectedImage.width() - 1) { @@ -96,7 +101,13 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co double numerator = (2.0 * mP * mQ + c1) * (2.0 * sigPQ + c2); double denominator = (mP * mP + mQ * mQ + c1) * (sigsqP + sigsqQ + c2); - ssim += numerator / denominator; + double value { numerator / denominator }; + _ssimResults.results.push_back(value); + ssim += value; + + if (value < min) min = value; + if (value > max) max = value; + ++windowCounter; y += WIN_SIZE; @@ -106,5 +117,17 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co y = 0; } - return ssim / windowCounter; -}; \ No newline at end of file + _ssimResults.width = (int)(expectedImage.width() / WIN_SIZE); + _ssimResults.height = (int)(expectedImage.height() / WIN_SIZE); + _ssimResults.min = min; + _ssimResults.max = max; + _ssimResults.ssim = ssim / windowCounter; +}; + +double ImageComparer::getSSIMValue() { + return _ssimResults.ssim; +} + +SSIMResults ImageComparer::getSSIMResults() { + return _ssimResults; +} diff --git a/tools/nitpick/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h index 7b7b8b0b74..fc14dab94d 100644 --- a/tools/nitpick/src/ImageComparer.h +++ b/tools/nitpick/src/ImageComparer.h @@ -10,12 +10,20 @@ #ifndef hifi_ImageComparer_h #define hifi_ImageComparer_h +#include "common.h" + #include #include class ImageComparer { public: - double compareImages(QImage resultImage, QImage expectedImage) const; + void compareImages(const QImage& resultImage, const QImage& expectedImage); + double getSSIMValue(); + + SSIMResults getSSIMResults(); + +private: + SSIMResults _ssimResults; }; #endif // hifi_ImageComparer_h diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp index 58189b4795..fd5df0dd4e 100644 --- a/tools/nitpick/src/MismatchWindow.cpp +++ b/tools/nitpick/src/MismatchWindow.cpp @@ -21,7 +21,7 @@ MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) { diffImage->setScaledContents(true); } -QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultImage) { +QPixmap MismatchWindow::computeDiffPixmap(const QImage& expectedImage, const QImage& resultImage) { // Create an empty difference image if the images differ in size if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) { return QPixmap(); @@ -60,7 +60,7 @@ QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultIma return resultPixmap; } -void MismatchWindow::setTestResult(TestResult testResult) { +void MismatchWindow::setTestResult(const TestResult& testResult) { errorLabel->setText("Similarity: " + QString::number(testResult._error)); imagePath->setText("Path to test: " + testResult._pathname); @@ -99,3 +99,36 @@ void MismatchWindow::on_abortTestsButton_clicked() { QPixmap MismatchWindow::getComparisonImage() { return _diffPixmap; } + +QPixmap MismatchWindow::getSSIMResultsImage(const SSIMResults& ssimResults) { + // This is an optimization, as QImage.setPixel() is embarrassingly slow + const int ELEMENT_SIZE { 8 }; + const int WIDTH{ ssimResults.width * ELEMENT_SIZE }; + const int HEIGHT{ ssimResults.height * ELEMENT_SIZE }; + + unsigned char* buffer = new unsigned char[WIDTH * HEIGHT * 3]; + + + // loop over each SSIM result + for (int y = 0; y < ssimResults.height; ++y) { + for (int x = 0; x < ssimResults.width; ++x) { + double scaledResult = (ssimResults.results[x * ssimResults.height + y] + 1.0) / (2.0); + //double scaledResult = (ssimResults.results[x * ssimResults.height + y] - ssimResults.min) / (ssimResults.max - ssimResults.min); + // Create a square + for (int yy = 0; yy < ELEMENT_SIZE; ++yy) { + for (int xx = 0; xx < ELEMENT_SIZE; ++xx) { + buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 0] = 255 * (1.0 - scaledResult); // R + buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 1] = 255 * scaledResult; // G + buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 2] = 0; // B + } + } + } + } + + QImage image(buffer, WIDTH, HEIGHT, QImage::Format_RGB888); + QPixmap pixmap = QPixmap::fromImage(image); + + delete[] buffer; + + return pixmap; +} diff --git a/tools/nitpick/src/MismatchWindow.h b/tools/nitpick/src/MismatchWindow.h index 040e0b8bf1..116d35dfc5 100644 --- a/tools/nitpick/src/MismatchWindow.h +++ b/tools/nitpick/src/MismatchWindow.h @@ -20,12 +20,14 @@ class MismatchWindow : public QDialog, public Ui::MismatchWindow { public: MismatchWindow(QWidget *parent = Q_NULLPTR); - void setTestResult(TestResult testResult); + void setTestResult(const TestResult& testResult); UserResponse getUserResponse() { return _userResponse; } - QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage); + QPixmap computeDiffPixmap(const QImage& expectedImage, const QImage& resultImage); + QPixmap getComparisonImage(); + QPixmap getSSIMResultsImage(const SSIMResults& ssimResults); private slots: void on_passTestButton_clicked(); diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 39800c6bc6..c07a76fc58 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -40,7 +40,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.0.0"); + setWindowTitle("Nitpick - v3.0.1"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); @@ -148,10 +148,6 @@ void Nitpick::on_tabWidget_currentChanged(int index) { } } -void Nitpick::on_evaluateTestsPushbutton_clicked() { - _test->startTestsEvaluation(false, false); -} - void Nitpick::on_createRecursiveScriptPushbutton_clicked() { _test->createRecursiveScript(); } @@ -242,6 +238,10 @@ void Nitpick::on_showTaskbarPushbutton_clicked() { #endif } +void Nitpick::on_evaluateTestsPushbutton_clicked() { + _test->startTestsEvaluation(false, false); +} + void Nitpick::on_closePushbutton_clicked() { exit(0); } @@ -255,7 +255,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() { } void Nitpick::on_createWebPagePushbutton_clicked() { - _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); + _test->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit); } void Nitpick::downloadFile(const QUrl& url) { diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 80fef934d6..3095a14c05 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -56,7 +56,6 @@ private slots: void on_tabWidget_currentChanged(int index); - void on_evaluateTestsPushbutton_clicked(); void on_createRecursiveScriptPushbutton_clicked(); void on_createAllRecursiveScriptsPushbutton_clicked(); void on_createTestsPushbutton_clicked(); @@ -82,6 +81,8 @@ private slots: void on_hideTaskbarPushbutton_clicked(); void on_showTaskbarPushbutton_clicked(); + void on_evaluateTestsPushbutton_clicked(); + void on_createPythonScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked(); diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index e8e284bf32..7269fb3f02 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -89,23 +89,25 @@ int Test::compareImageLists() { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); similarityIndex = -100.0; } else { - similarityIndex = _imageComparer.compareImages(resultImage, expectedImage); + _imageComparer.compareImages(resultImage, expectedImage); + similarityIndex = _imageComparer.getSSIMValue(); } TestResult testResult = TestResult{ (float)similarityIndex, _expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image - QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName() // filename of result image + QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of result image + _imageComparer.getSSIMResults() // results of SSIM algoritm }; _mismatchWindow.setTestResult(testResult); - + if (similarityIndex < THRESHOLD) { ++numberOfFailures; if (!isInteractiveMode) { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); } else { _mismatchWindow.exec(); @@ -113,7 +115,7 @@ int Test::compareImageLists() { case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); break; case USER_RESPONSE_ABORT: keepOn = false; @@ -124,7 +126,7 @@ int Test::compareImageLists() { } } } else { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), false); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), false); } _progressBar->setValue(i); @@ -156,7 +158,7 @@ int Test::checkTextResults() { return testsFailed.length(); } -void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed) { +void Test::appendTestResultsToFile(const TestResult& testResult, const QPixmap& comparisonImage, const QPixmap& ssimResultsImage, bool hasFailed) { // Critical error if Test Results folder does not exist if (!QDir().exists(_testResultsFolderPath)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found"); @@ -217,6 +219,9 @@ void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImag } comparisonImage.save(resultFolderPath + "/" + "Difference Image.png"); + + // Save the SSIM results image + ssimResultsImage.save(resultFolderPath + "/" + "SSIM Image.png"); } void::Test::appendTestResultsToFile(QString testResultFilename, bool hasFailed) { @@ -1095,7 +1100,12 @@ void Test::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) { _testRailCreateMode = testRailCreateMode; } -void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { +void Test::createWebPage( + QCheckBox* updateAWSCheckBox, + QRadioButton* diffImageRadioButton, + QRadioButton* ssimImageRadionButton, + QLineEdit* urlLineEdit +) { QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, "Zipped Test Results (TestResults--*.zip)"); if (testResults.isNull()) { @@ -1112,5 +1122,12 @@ void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { _awsInterface = new AWSInterface; } - _awsInterface->createWebPageFromResults(testResults, workingDirectory, updateAWSCheckBox, urlLineEdit); + _awsInterface->createWebPageFromResults( + testResults, + workingDirectory, + updateAWSCheckBox, + diffImageRadioButton, + ssimImageRadionButton, + urlLineEdit + ); } \ No newline at end of file diff --git a/tools/nitpick/src/Test.h b/tools/nitpick/src/Test.h index 23011d0c31..8753b9fcda 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -87,7 +87,7 @@ public: void includeTest(QTextStream& textStream, const QString& testPathname); - void appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed); + void appendTestResultsToFile(const TestResult& testResult, const QPixmap& comparisonImage, const QPixmap& ssimResultsImage, bool hasFailed); void appendTestResultsToFile(QString testResultFilename, bool hasFailed); bool createTestResultsFolderPath(const QString& directory); @@ -102,7 +102,11 @@ public: void setTestRailCreateMode(TestRailCreateMode testRailCreateMode); - void createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit); + void createWebPage( + QCheckBox* updateAWSCheckBox, + QRadioButton* diffImageRadioButton, + QRadioButton* ssimImageRadionButton, + QLineEdit* urlLineEdit); private: QProgressBar* _progressBar; @@ -116,7 +120,7 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD{ 0.935 }; + const double THRESHOLD{ 0.98 }; QDir _imageDirectory; diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 5df4e9c921..eb228ff2b3 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -10,21 +10,38 @@ #ifndef hifi_common_h #define hifi_common_h +#include #include +class SSIMResults { +public: + int width; + int height; + std::vector results; + double ssim; + + // Used for scaling + double min; + double max; +}; + class TestResult { public: - TestResult(float error, QString pathname, QString expectedImageFilename, QString actualImageFilename) : + TestResult(float error, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : _error(error), _pathname(pathname), _expectedImageFilename(expectedImageFilename), - _actualImageFilename(actualImageFilename) + _actualImageFilename(actualImageFilename), + _ssimResults(ssimResults) {} double _error; + QString _pathname; QString _expectedImageFilename; QString _actualImageFilename; + + SSIMResults _ssimResults; }; enum UserResponse { diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 47471522db..1857a2118f 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -43,7 +43,7 @@ - 0 + 5 @@ -760,7 +760,7 @@ 190 - 180 + 200 131 20 @@ -776,7 +776,7 @@ 330 - 170 + 190 181 51 @@ -889,8 +889,8 @@ - 270 - 30 + 370 + 20 160 51 @@ -922,6 +922,38 @@ + + + + 260 + 50 + 95 + 20 + + + + Diff Image + + + false + + + + + + 260 + 30 + 95 + 20 + + + + SSIM Image + + + true + + groupBox updateTestRailRunResultsPushbutton