Merge branch 'master' of https://github.com/highfidelity/hifi into blackProp

This commit is contained in:
Sam Gateau 2019-03-06 18:42:11 -08:00
commit 26b581fc74
53 changed files with 1082 additions and 434 deletions

View file

@ -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<void(AudioMixerSlave& slave)> 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
}

View file

@ -23,6 +23,7 @@
#include <QtCore/QRegularExpression>
#include <QtCore/QTimer>
#include <QtCore/QThread>
#include <QtCore/QJsonDocument>
#include <AABox.h>
#include <AvatarLogging.h>
@ -32,6 +33,8 @@
#include <SharedUtil.h>
#include <UUID.h>
#include <TryLocker.h>
#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<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
DependencyManager::set<AssignmentDynamicFactory>();
// make sure we hear about node kills so we can tell the other nodes
connect(DependencyManager::get<NodeList>().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<ReceivedMessage
}
void AvatarMixer::sendStatsPacket() {
auto start = usecTimestampNow();
if (!_numTightLoopFrames) {
return;
}
auto start = usecTimestampNow();
QJsonObject statsObject;
@ -775,6 +798,7 @@ void AvatarMixer::sendStatsPacket() {
slavesAggregatObject["sent_4_averageDataBytes"] = TIGHT_LOOP_STAT(aggregateStats.numDataBytesSent);
slavesAggregatObject["sent_5_averageTraitsBytes"] = TIGHT_LOOP_STAT(aggregateStats.numTraitsBytesSent);
slavesAggregatObject["sent_6_averageIdentityBytes"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityBytesSent);
slavesAggregatObject["sent_7_averageHeroAvatars"] = TIGHT_LOOP_STAT(aggregateStats.numHeroesIncluded);
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
@ -882,13 +906,15 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node
void AvatarMixer::domainSettingsRequestComplete() {
auto nodeList = DependencyManager::get<NodeList>();
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<NodeList>();
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<SpatialParentFinder, AssignmentParentFinder>();
DependencyManager::set<AssignmentParentFinder>(_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<ReceivedMessage> 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<NLPacket> for it
auto buffer = std::unique_ptr<char[]>(new char[piggyBackedSizeWithHeader]);
memcpy(buffer.get(), message->getRawMessage() + message->getPosition(), piggyBackedSizeWithHeader);
auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggyBackedSizeWithHeader, message->getSenderSockAddr());
auto newMessage = QSharedPointer<ReceivedMessage>::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<AssignmentDynamicFactory>();
DependencyManager::destroy<AssignmentParentFinder>();
ThreadedAssignment::aboutToFinish();
}

View file

@ -20,6 +20,7 @@
#include <PortableHighResolutionClock.h>
#include <ThreadedAssignment.h>
#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<ReceivedMessage> message);
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void handleOctreePacket(QSharedPointer<ReceivedMessage> 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 };

View file

@ -16,6 +16,10 @@
#include <DependencyManager.h>
#include <NodeList.h>
#include <EntityTree.h>
#include <ZoneEntityItem.h>
#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<float>::max() };
static bool operation(const OctreeElementPointer& element, void* extraData) {
auto findPriorityZone = static_cast<FindPriorityZone*>(extraData);
if (element->getAACube().contains(findPriorityZone->position)) {
const EntityTreeElementPointer entityTreeElement = static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->forEachEntity([&findPriorityZone](EntityItemPointer item) {
if (item->getType() == EntityTypes::Zone
&& item->contains(findPriorityZone->position)) {
auto zoneItem = static_pointer_cast<ZoneEntityItem>(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,

View file

@ -21,7 +21,7 @@
#include <QtCore/QJsonObject>
#include <QtCore/QUrl>
#include <AvatarData.h>
#include "MixerAvatar.h"
#include <AssociatedTraitValues.h>
#include <NodeData.h>
#include <NumericalConstants.h>
@ -45,11 +45,12 @@ public:
using HRCTime = p_high_resolution_clock::time_point;
using PerNodeTraitVersions = std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions>;
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<NLPacket::LocalID, uint16_t> _lastBroadcastSequenceNumbers;

View file

@ -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<NodeList>();
@ -293,29 +320,30 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
_stats.nodesBroadcastedTo++;
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(destinationNode->getLinkedData());
AvatarMixerClientData* destinationNodeData = reinterpret_cast<AvatarMixerClientData*>(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<SortableAvatar> sortedAvatars(cameraViews,
AvatarData::_avatarSortCoefficientSize,
AvatarData::_avatarSortCoefficientCenter,
AvatarData::_avatarSortCoefficientAge);
sortedAvatars.reserve(_end - _begin);
const auto& cameraViews = destinationNodeData->getViewFrustums();
using AvatarPriorityQueue = PrioritySortUtil::PriorityQueue<SortableAvatar>;
// 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<const AvatarMixerClientData*>(avatarNode->getLinkedData());
assert(avatarClientNodeData); // we can't have gotten here without avatarNode having valid data
const AvatarMixerClientData* sourceAvatarNodeData = reinterpret_cast<const AvatarMixerClientData*>(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<const AvatarMixerClientData*>(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<JointData>& 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<chrono::microseconds>(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<chrono::microseconds>(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<const AvatarMixerClientData*>(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<JointData>& 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<chrono::microseconds>(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<chrono::microseconds>(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);

View file

@ -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<EntityTree>;
struct SlaveSharedData {
QStringList skeletonURLWhitelist;
QUrl skeletonReplacementURL;
EntityTreePointer entityTree;
};
class AvatarMixerSlave {

View file

@ -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<void(AvatarMixerSlave& slave)> 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
}

View file

@ -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 <AvatarData.h>
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<MixerAvatar>;
#endif // hifi_MixerAvatar_h

View file

@ -1203,7 +1203,8 @@ void OctreeServer::beginRunning() {
auto nodeList = DependencyManager::get<NodeList>();
// 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

View file

@ -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
}
]
},

View file

@ -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

View file

@ -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, <a href='#'>learn more</a>."
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: "<a href='toggle'>View all errors</a>"
onLinkActivated: {
avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport;
}
}
HifiControls.Button {
id: openFolderButton

View file

@ -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.

View file

@ -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);

View file

@ -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;

View file

@ -41,6 +41,7 @@
#include "EntitySimulation.h"
#include "EntityDynamicFactoryInterface.h"
//#define WANT_DEBUG
Q_DECLARE_METATYPE(EntityItemPointer);
int entityItemPointernMetaTypeId = qRegisterMetaType<EntityItemPointer>();
@ -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

View file

@ -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; }

View file

@ -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<ComponentPair, COMPONENT_MODE_ITEM_COUNT>::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.<br />
*
* @property {string} avatarPriority="inherit" - Configures the update priority of contained avatars to other clients.<br />
* <code>"inherit"</code>: Priority from enclosing zones is unchanged.<br />
* <code>"crowd"</code>: Priority in this zone is the normal priority.<br />
* <code>"hero"</code>: Avatars in this zone will have an increased update priority
* <pre>
*
* 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<QString> EntityItemProperties::listChangedProperties() {
if (bloomModeChanged()) {
out += "bloomMode";
}
if (avatarPriorityChanged()) {
out += "avatarPriority";
}
// Polyvox
if (voxelVolumeSizeChanged()) {

View file

@ -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, "");

View file

@ -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,

View file

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

View file

@ -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<KeyLightPropertyGroup>([&] { return _keyLightProperties; }); }
AmbientLightPropertyGroup getAmbientLightProperties() const { return resultWithReadLock<AmbientLightPropertyGroup>([&] { 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 };

View file

@ -117,7 +117,7 @@ namespace baker {
class BakerEngineBuilder {
public:
using Input = VaryingSet2<hfm::Model::Pointer, QVariantHash>;
using Input = VaryingSet2<hfm::Model::Pointer, GeometryMappingPair>;
using Output = VaryingSet2<hfm::Model::Pointer, MaterialMapping>;
using JobModel = Task::ModelIO<BakerEngineBuilder, Input, Output>;
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<Engine>(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared<BakeContext>())) {
_engine->feedInput<BakerEngineBuilder::Input>(0, hfmModel);
_engine->feedInput<BakerEngineBuilder::Input>(1, mapping);

View file

@ -17,13 +17,14 @@
#include <hfm/HFM.h>
#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();

View file

@ -12,6 +12,7 @@
#ifndef hifi_BakerTypes_h
#define hifi_BakerTypes_h
#include <QUrl>
#include <hfm/HFM.h>
namespace baker {
@ -35,6 +36,7 @@ namespace baker {
using TangentsPerBlendshape = std::vector<std::vector<glm::vec3>>;
using MeshIndicesToModelNames = QHash<int, QString>;
using GeometryMappingPair = std::pair<QUrl, QVariantHash>;
};
#endif // hifi_BakerTypes_h

View file

@ -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<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), materialResource));
}
} else if (mappingJSON.isString()) {
auto mappingValue = mappingJSON.toString();
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), MaterialCache::instance().getMaterial(mappingValue)));
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), MaterialCache::instance().getMaterial(url.resolved(mappingValue))));
}
}
}

View file

@ -14,12 +14,13 @@
#include <hfm/HFM.h>
#include "Engine.h"
#include "BakerTypes.h"
#include <material-networking/MaterialCache.h>
class ParseMaterialMappingTask {
public:
using Input = QVariantHash;
using Input = baker::GeometryMappingPair;
using Output = MaterialMapping;
using JobModel = baker::Job::ModelIO<ParseMaterialMappingTask, Input, Output>;

View file

@ -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;

View file

@ -17,10 +17,11 @@
#include <hfm/HFM.h>
#include "Engine.h"
#include "BakerTypes.h"
class PrepareJointsTask {
public:
using Input = baker::VaryingSet2<std::vector<hfm::Joint>, QVariantHash /*mapping*/>;
using Input = baker::VaryingSet2<std::vector<hfm::Joint>, baker::GeometryMappingPair /*mapping*/>;
using Output = baker::VaryingSet3<std::vector<hfm::Joint>, QMap<int, glm::quat> /*jointRotationOffsets*/, QHash<QString, int> /*jointIndices*/>;
using JobModel = baker::Job::ModelIO<PrepareJointsTask, Input, Output>;

View file

@ -35,11 +35,13 @@ class GeometryReader;
class GeometryExtra {
public:
const QVariantHash& mapping;
const GeometryMappingPair& mapping;
const QUrl& textureBaseUrl;
bool combineParts;
};
int geometryMappingPairTypeId = qRegisterMetaType<GeometryMappingPair>("GeometryMappingPair");
// From: https://stackoverflow.com/questions/41145012/how-to-hash-qvariant
class QVariantHasher {
public:
@ -78,7 +80,8 @@ namespace std {
struct hash<GeometryExtra> {
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<ModelCache>();
GeometryExtra extra { _mapping, _textureBaseUrl, false };
GeometryExtra extra { GeometryMappingPair(_url, _mapping), _textureBaseUrl, false };
// Get the raw GeometryResource
_geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash<GeometryExtra>()(extra)).staticCast<GeometryResource>();
@ -191,7 +194,7 @@ void GeometryMappingResource::onGeometryMappingLoaded(bool success) {
class GeometryReader : public QRunnable {
public:
GeometryReader(const ModelLoader& modelLoader, QWeakPointer<Resource>& resource, const QUrl& url, const QVariantHash& mapping,
GeometryReader(const ModelLoader& modelLoader, QWeakPointer<Resource>& 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> _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<const GeometryExtra*>(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<Resource> ModelCache::createResource(const QUrl& url) {
}
QSharedPointer<Resource> ModelCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
return QSharedPointer<Resource>(new GeometryDefinitionResource(*resource.staticCast<GeometryDefinitionResource>()), &Resource::deleter);
if (resource->getURL().path().toLower().endsWith(".fst")) {
return QSharedPointer<Resource>(new GeometryMappingResource(*resource.staticCast<GeometryMappingResource>()), &Resource::deleter);
} else {
return QSharedPointer<Resource>(new GeometryDefinitionResource(*resource.staticCast<GeometryDefinitionResource>()), &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>()(geometryExtra)).staticCast<GeometryResource>();
@ -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>()(geometryExtra)).staticCast<GeometryResource>();

View file

@ -26,6 +26,9 @@ class MeshPart;
class GeometryMappingResource;
using GeometryMappingPair = std::pair<QUrl, QVariantHash>;
Q_DECLARE_METATYPE(GeometryMappingPair)
class Geometry {
public:
using Pointer = std::shared_ptr<Geometry>;
@ -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:

View file

@ -40,6 +40,9 @@
static Setting::Handle<quint16> 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<NodeType_t> 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<SharedNodePointer> killedNodes;
std::vector<SharedNodePointer> 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<NLPacket> 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<NLPacket> 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;
}
});

View file

@ -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<size_t>::max() };
extern const std::set<NodeType_t> 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<SharedNodePointer> 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<SharedNodePointer> 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<NewNodeInfo> _delayedNodeAdds;
int _inboundPPS { 0 };
int _outboundPPS { 0 };
float _inboundKbps { 0.0f };

View file

@ -200,7 +200,6 @@ void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer&
}
void NodeList::processPingPacket(QSharedPointer<ReceivedMessage> 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<AccountManager>();
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<AddressManager>()->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<ReceivedMessage> 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() {

View file

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

View file

@ -260,6 +260,7 @@ enum class EntityVersion : PacketVersion {
MissingWebEntityProperties,
PulseProperties,
RingGizmoEntities,
AvatarPriorityZone,
ShowKeyboardFocusHighlight,
WebBillboardMode,
ModelScale,

View file

@ -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."
},

View file

@ -382,7 +382,8 @@ const DEFAULT_ENTITY_PROPERTIES = {
},
},
shapeType: "box",
bloomMode: "inherit"
bloomMode: "inherit",
avatarPriority: "inherit"
},
Model: {
collisionShape: "none",

View file

@ -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",
},
]
},
{

View file

@ -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) {

View file

@ -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<th><h1>Test</h1></th>\n";
stream << "\t\t\t\t<th><h1>Actual Image</h1></th>\n";
stream << "\t\t\t\t<th><h1>Expected Image</h1></th>\n";
stream << "\t\t\t\t<th><h1>Difference Image</h1></th>\n";
stream << "\t\t\t\t<th><h1>Comparison Image</h1></th>\n";
stream << "\t\t\t</tr>\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<td><img src=\"./" << folder << "/" << resultName << "/Expected Image.png\" width = \"576\" height = \"324\" ></td>\n";
if (differenceFileFound) {
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Difference Image.png\" width = \"576\" height = \"324\" ></td>\n";
stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/" << _comparisonImageFilename << "\" width = \"576\" height = \"324\" ></td>\n";
} else {
stream << "\t\t\t\t<td><h2>No Image Found</h2>\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";
}
}
}

View file

@ -14,6 +14,7 @@
#include <QCheckBox>
#include <QLineEdit>
#include <QObject>
#include <QRadioButton>
#include <QTextStream>
#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

View file

@ -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;
};
_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;
}

View file

@ -10,12 +10,20 @@
#ifndef hifi_ImageComparer_h
#define hifi_ImageComparer_h
#include "common.h"
#include <QtCore/QString>
#include <QImage>
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

View file

@ -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;
}

View file

@ -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();

View file

@ -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) {

View file

@ -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();

View file

@ -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
);
}

View file

@ -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;

View file

@ -10,21 +10,38 @@
#ifndef hifi_common_h
#define hifi_common_h
#include <vector>
#include <QtCore/QString>
class SSIMResults {
public:
int width;
int height;
std::vector<double> 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 {

View file

@ -43,7 +43,7 @@
</rect>
</property>
<property name="currentIndex">
<number>0</number>
<number>5</number>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
@ -760,7 +760,7 @@
<property name="geometry">
<rect>
<x>190</x>
<y>180</y>
<y>200</y>
<width>131</width>
<height>20</height>
</rect>
@ -776,7 +776,7 @@
<property name="geometry">
<rect>
<x>330</x>
<y>170</y>
<y>190</y>
<width>181</width>
<height>51</height>
</rect>
@ -889,8 +889,8 @@
</property>
<property name="geometry">
<rect>
<x>270</x>
<y>30</y>
<x>370</x>
<y>20</y>
<width>160</width>
<height>51</height>
</rect>
@ -922,6 +922,38 @@
</rect>
</property>
</widget>
<widget class="QRadioButton" name="diffImageRadioButton">
<property name="geometry">
<rect>
<x>260</x>
<y>50</y>
<width>95</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string>Diff Image</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
<widget class="QRadioButton" name="ssimImageRadioButton">
<property name="geometry">
<rect>
<x>260</x>
<y>30</y>
<width>95</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string>SSIM Image</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</widget>
<zorder>groupBox</zorder>
<zorder>updateTestRailRunResultsPushbutton</zorder>