Merge branch 'master' of github.com:highfidelity/hifi into 21559-increaseComparisonThreshold

This commit is contained in:
NissimHadar 2019-03-07 11:32:12 -08:00
commit 53d682d40e
66 changed files with 1608 additions and 711 deletions

View file

@ -64,10 +64,6 @@ bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) {
return _pool._queue.try_pop(node); return _pool._queue.try_pop(node);
} }
#ifdef AUDIO_SINGLE_THREADED
static AudioMixerSlave slave;
#endif
void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) { void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) {
_function = &AudioMixerSlave::processPackets; _function = &AudioMixerSlave::processPackets;
_configure = [](AudioMixerSlave& slave) {}; _configure = [](AudioMixerSlave& slave) {};
@ -87,19 +83,9 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) {
_begin = begin; _begin = begin;
_end = end; _end = end;
#ifdef AUDIO_SINGLE_THREADED
_configure(slave);
std::for_each(begin, end, [&](const SharedNodePointer& node) {
_function(slave, node);
});
#else
// fill the queue // fill the queue
std::for_each(_begin, _end, [&](const SharedNodePointer& node) { std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
#if defined(__clang__) && defined(Q_OS_LINUX)
_queue.push(node); _queue.push(node);
#else
_queue.emplace(node);
#endif
}); });
{ {
@ -119,17 +105,12 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) {
} }
assert(_queue.empty()); assert(_queue.empty());
#endif
} }
void AudioMixerSlavePool::each(std::function<void(AudioMixerSlave& slave)> functor) { void AudioMixerSlavePool::each(std::function<void(AudioMixerSlave& slave)> functor) {
#ifdef AUDIO_SINGLE_THREADED
functor(slave);
#else
for (auto& slave : _slaves) { for (auto& slave : _slaves) {
functor(*slave.get()); functor(*slave.get());
} }
#endif
} }
void AudioMixerSlavePool::setNumThreads(int numThreads) { void AudioMixerSlavePool::setNumThreads(int numThreads) {
@ -155,9 +136,6 @@ void AudioMixerSlavePool::setNumThreads(int numThreads) {
void AudioMixerSlavePool::resize(int numThreads) { void AudioMixerSlavePool::resize(int numThreads) {
assert(_numThreads == (int)_slaves.size()); 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); qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads);
Lock lock(_mutex); Lock lock(_mutex);
@ -205,5 +183,4 @@ void AudioMixerSlavePool::resize(int numThreads) {
_numThreads = _numStarted = _numFinished = numThreads; _numThreads = _numStarted = _numFinished = numThreads;
assert(_numThreads == (int)_slaves.size()); assert(_numThreads == (int)_slaves.size());
#endif
} }

View file

@ -23,6 +23,7 @@
#include <QtCore/QRegularExpression> #include <QtCore/QRegularExpression>
#include <QtCore/QTimer> #include <QtCore/QTimer>
#include <QtCore/QThread> #include <QtCore/QThread>
#include <QtCore/QJsonDocument>
#include <AABox.h> #include <AABox.h>
#include <AvatarLogging.h> #include <AvatarLogging.h>
@ -32,6 +33,8 @@
#include <SharedUtil.h> #include <SharedUtil.h>
#include <UUID.h> #include <UUID.h>
#include <TryLocker.h> #include <TryLocker.h>
#include "../AssignmentDynamicFactory.h"
#include "../entities/AssignmentParentFinder.h"
const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer";
@ -55,6 +58,9 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
ThreadedAssignment(message), ThreadedAssignment(message),
_slavePool(&_slaveSharedData) _slavePool(&_slaveSharedData)
{ {
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
DependencyManager::set<AssignmentDynamicFactory>();
// make sure we hear about node kills so we can tell the other nodes // make sure we hear about node kills so we can tell the other nodes
connect(DependencyManager::get<NodeList>().data(), &NodeList::nodeKilled, this, &AvatarMixer::handleAvatarKilled); 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::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket"); packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket"); packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
this, "handleOctreePacket");
packetReceiver.registerListenerForTypes({ packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity, PacketType::ReplicatedAvatarIdentity,
@ -240,6 +248,10 @@ void AvatarMixer::start() {
int lockWait, nodeTransform, functor; int lockWait, nodeTransform, functor;
{
_entityViewer.queryOctree();
}
// Allow nodes to process any pending/queued packets across our worker threads // Allow nodes to process any pending/queued packets across our worker threads
{ {
auto start = usecTimestampNow(); auto start = usecTimestampNow();
@ -252,6 +264,10 @@ void AvatarMixer::start() {
}, &lockWait, &nodeTransform, &functor); }, &lockWait, &nodeTransform, &functor);
auto end = usecTimestampNow(); auto end = usecTimestampNow();
_processQueuedAvatarDataPacketsElapsedTime += (end - start); _processQueuedAvatarDataPacketsElapsedTime += (end - start);
_broadcastAvatarDataLockWait += lockWait;
_broadcastAvatarDataNodeTransform += nodeTransform;
_broadcastAvatarDataNodeFunctor += functor;
} }
// process pending display names... this doesn't currently run on multiple threads, because it // process pending display names... this doesn't currently run on multiple threads, because it
@ -269,6 +285,10 @@ void AvatarMixer::start() {
}, &lockWait, &nodeTransform, &functor); }, &lockWait, &nodeTransform, &functor);
auto end = usecTimestampNow(); auto end = usecTimestampNow();
_displayNameManagementElapsedTime += (end - start); _displayNameManagementElapsedTime += (end - start);
_broadcastAvatarDataLockWait += lockWait;
_broadcastAvatarDataNodeTransform += nodeTransform;
_broadcastAvatarDataNodeFunctor += functor;
} }
// this is where we need to put the real work... // this is where we need to put the real work...
@ -691,8 +711,11 @@ void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage
} }
void AvatarMixer::sendStatsPacket() { void AvatarMixer::sendStatsPacket() {
auto start = usecTimestampNow(); if (!_numTightLoopFrames) {
return;
}
auto start = usecTimestampNow();
QJsonObject statsObject; QJsonObject statsObject;
@ -775,6 +798,7 @@ void AvatarMixer::sendStatsPacket() {
slavesAggregatObject["sent_4_averageDataBytes"] = TIGHT_LOOP_STAT(aggregateStats.numDataBytesSent); slavesAggregatObject["sent_4_averageDataBytes"] = TIGHT_LOOP_STAT(aggregateStats.numDataBytesSent);
slavesAggregatObject["sent_5_averageTraitsBytes"] = TIGHT_LOOP_STAT(aggregateStats.numTraitsBytesSent); slavesAggregatObject["sent_5_averageTraitsBytes"] = TIGHT_LOOP_STAT(aggregateStats.numTraitsBytesSent);
slavesAggregatObject["sent_6_averageIdentityBytes"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityBytesSent); 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_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime); slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
@ -882,13 +906,15 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node
void AvatarMixer::domainSettingsRequestComplete() { void AvatarMixer::domainSettingsRequestComplete() {
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
nodeList->addSetOfNodeTypesToNodeInterestSet({ nodeList->addSetOfNodeTypesToNodeInterestSet({
NodeType::Agent, NodeType::EntityScriptServer, NodeType::Agent, NodeType::EntityScriptServer, NodeType::EntityServer,
NodeType::UpstreamAvatarMixer, NodeType::DownstreamAvatarMixer NodeType::UpstreamAvatarMixer, NodeType::DownstreamAvatarMixer
}); });
// parse the settings to pull out the values we need // parse the settings to pull out the values we need
parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject()); parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject());
setupEntityQuery();
// start our tight loop... // start our tight loop...
start(); 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."; 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"; const QString AVATARS_SETTINGS_KEY = "avatars";
static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; 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()); 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 <PortableHighResolutionClock.h>
#include <ThreadedAssignment.h> #include <ThreadedAssignment.h>
#include "../entities/EntityTreeHeadlessViewer.h"
#include "AvatarMixerClientData.h" #include "AvatarMixerClientData.h"
#include "AvatarMixerSlavePool.h" #include "AvatarMixerSlavePool.h"
@ -29,6 +30,7 @@ class AvatarMixer : public ThreadedAssignment {
Q_OBJECT Q_OBJECT
public: public:
AvatarMixer(ReceivedMessage& message); AvatarMixer(ReceivedMessage& message);
virtual void aboutToFinish() override;
static bool shouldReplicateTo(const Node& from, const Node& to) { static bool shouldReplicateTo(const Node& from, const Node& to) {
return to.getType() == NodeType::DownstreamAvatarMixer && return to.getType() == NodeType::DownstreamAvatarMixer &&
@ -57,6 +59,7 @@ private slots:
void handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message); void handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message);
void domainSettingsRequestComplete(); void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void start(); void start();
private: private:
@ -71,8 +74,13 @@ private:
void optionallyReplicatePacket(ReceivedMessage& message, const Node& node); void optionallyReplicatePacket(ReceivedMessage& message, const Node& node);
void setupEntityQuery();
p_high_resolution_clock::time_point _lastFrameTimestamp; 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 // FIXME - new throttling - use these values somehow
float _trailingMixRatio { 0.0f }; float _trailingMixRatio { 0.0f };
float _throttlingRatio { 0.0f }; float _throttlingRatio { 0.0f };

View file

@ -16,6 +16,10 @@
#include <DependencyManager.h> #include <DependencyManager.h>
#include <NodeList.h> #include <NodeList.h>
#include <EntityTree.h>
#include <ZoneEntityItem.h>
#include "AvatarLogging.h"
#include "AvatarMixerSlave.h" #include "AvatarMixerSlave.h"
@ -62,7 +66,7 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
switch (packet->getType()) { switch (packet->getType()) {
case PacketType::AvatarData: case PacketType::AvatarData:
parseData(*packet); parseData(*packet, slaveSharedData);
break; break;
case PacketType::SetAvatarTraits: case PacketType::SetAvatarTraits:
processSetTraitsMessage(*packet, slaveSharedData, *node); processSetTraitsMessage(*packet, slaveSharedData, *node);
@ -80,7 +84,42 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
return packetsProcessed; 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 // pull the sequence number from the data first
uint16_t sequenceNumber; uint16_t sequenceNumber;
@ -90,9 +129,33 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) {
incrementNumOutOfOrderSends(); incrementNumOutOfOrderSends();
} }
_lastReceivedSequenceNumber = sequenceNumber; _lastReceivedSequenceNumber = sequenceNumber;
glm::vec3 oldPosition = getPosition();
// compute the offset to the data payload // 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, void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,

View file

@ -21,7 +21,7 @@
#include <QtCore/QJsonObject> #include <QtCore/QJsonObject>
#include <QtCore/QUrl> #include <QtCore/QUrl>
#include <AvatarData.h> #include "MixerAvatar.h"
#include <AssociatedTraitValues.h> #include <AssociatedTraitValues.h>
#include <NodeData.h> #include <NodeData.h>
#include <NumericalConstants.h> #include <NumericalConstants.h>
@ -45,11 +45,12 @@ public:
using HRCTime = p_high_resolution_clock::time_point; using HRCTime = p_high_resolution_clock::time_point;
using PerNodeTraitVersions = std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions>; using PerNodeTraitVersions = std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions>;
int parseData(ReceivedMessage& message) override; using NodeData::parseData; // Avoid clang warning about hiding.
AvatarData& getAvatar() { return *_avatar; } int parseData(ReceivedMessage& message, const SlaveSharedData& SlaveSharedData);
const AvatarData& getAvatar() const { return *_avatar; } MixerAvatar& getAvatar() { return *_avatar; }
const AvatarData* getConstAvatarData() const { return _avatar.get(); } const MixerAvatar& getAvatar() const { return *_avatar; }
AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } const MixerAvatar* getConstAvatarData() const { return _avatar.get(); }
MixerAvatarSharedPointer getAvatarSharedPointer() const { return _avatar; }
uint16_t getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const; uint16_t getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const;
void setLastBroadcastSequenceNumber(NLPacket::LocalID nodeID, uint16_t sequenceNumber) void setLastBroadcastSequenceNumber(NLPacket::LocalID nodeID, uint16_t sequenceNumber)
@ -163,7 +164,7 @@ private:
}; };
PacketQueue _packetQueue; PacketQueue _packetQueue;
AvatarSharedPointer _avatar { new AvatarData() }; MixerAvatarSharedPointer _avatar { new MixerAvatar() };
uint16_t _lastReceivedSequenceNumber { 0 }; uint16_t _lastReceivedSequenceNumber { 0 };
std::unordered_map<NLPacket::LocalID, uint16_t> _lastBroadcastSequenceNumbers; std::unordered_map<NLPacket::LocalID, uint16_t> _lastBroadcastSequenceNumbers;

View file

@ -281,7 +281,34 @@ AABox computeBubbleBox(const AvatarData& avatar, float bubbleExpansionFactor) {
return box; 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) { void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) {
const float AVATAR_HERO_FRACTION { 0.4f };
const Node* destinationNode = node.data(); const Node* destinationNode = node.data();
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
@ -293,29 +320,30 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
_stats.nodesBroadcastedTo++; _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(); const AvatarData& avatar = destinationNodeData->getAvatar();
glm::vec3 myPosition = avatar.getClientGlobalPosition(); glm::vec3 destinationPosition = avatar.getClientGlobalPosition();
// reset the internal state for correct random number distribution // reset the internal state for correct random number distribution
distribution.reset(); distribution.reset();
// Estimate number to sort on number sent last frame (with min. of 20). // 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 // reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame(); destinationNodeData->resetNumAvatarsSentLastFrame();
// keep track of outbound data rate specifically for avatar data // keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0; int numAvatarDataBytes = 0;
int identityBytesSent = 0; int identityBytesSent = 0;
int traitBytesSent = 0; int traitBytesSent = 0;
// max number of avatarBytes per frame // max number of avatarBytes per frame (13 900, typical)
int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); 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 // keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0; 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 // When this is true, the AvatarMixer will send Avatar data to a client
// about avatars they've ignored or that are out of view // about avatars they've ignored or that are out of view
bool PALIsOpen = nodeData->getRequestsDomainListData(); bool PALIsOpen = destinationNodeData->getRequestsDomainListData();
bool PALWasOpen = nodeData->getPrevRequestsDomainListData(); bool PALWasOpen = destinationNodeData->getPrevRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them // When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = PALIsOpen && destinationNode->getCanKick(); bool getsAnyIgnored = PALIsOpen && destinationNode->getCanKick();
@ -337,36 +365,23 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// compute node bounding box // compute node bounding box
const float MY_AVATAR_BUBBLE_EXPANSION_FACTOR = 4.0f; // magic number determined emperically const float MY_AVATAR_BUBBLE_EXPANSION_FACTOR = 4.0f; // magic number determined emperically
AABox nodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR); AABox destinationNodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR);
class SortableAvatar: public PrioritySortUtil::Sortable {
public:
SortableAvatar() = delete;
SortableAvatar(const AvatarData* avatar, const Node* avatarNode, uint64_t lastEncodeTime)
: _avatar(avatar), _node(avatarNode), _lastEncodeTime(lastEncodeTime) {}
glm::vec3 getPosition() const override { return _avatar->getClientGlobalPosition(); }
float getRadius() const override {
glm::vec3 nodeBoxScale = _avatar->getGlobalBoundingBox().getScale();
return 0.5f * glm::max(nodeBoxScale.x, glm::max(nodeBoxScale.y, nodeBoxScale.z));
}
uint64_t getTimestamp() const override {
return _lastEncodeTime;
}
const Node* getNode() const { return _node; }
private:
const AvatarData* _avatar;
const Node* _node;
uint64_t _lastEncodeTime;
};
// prepare to sort // prepare to sort
const auto& cameraViews = nodeData->getViewFrustums(); const auto& cameraViews = destinationNodeData->getViewFrustums();
PrioritySortUtil::PriorityQueue<SortableAvatar> sortedAvatars(cameraViews,
AvatarData::_avatarSortCoefficientSize, using AvatarPriorityQueue = PrioritySortUtil::PriorityQueue<SortableAvatar>;
AvatarData::_avatarSortCoefficientCenter, // Keep two independent queues, one for heroes and one for the riff-raff.
AvatarData::_avatarSortCoefficientAge); enum PriorityVariants { kHero, kNonhero };
sortedAvatars.reserve(_end - _begin); 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) { for (auto listedNode = _begin; listedNode != _end; ++listedNode) {
Node* otherNodeRaw = (*listedNode).data(); Node* otherNodeRaw = (*listedNode).data();
@ -376,47 +391,47 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
continue; 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: // We ignore other nodes for a couple of reasons:
// 1) ignore bubbles and ignore specific node // 1) ignore bubbles and ignore specific node
// 2) the node hasn't really updated it's frame data recently, this can // 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 // happen if for example the avatar is connected on a desktop and sending
// updates at ~30hz. So every 3 frames we skip a frame. // 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()); const AvatarMixerClientData* sourceAvatarNodeData = reinterpret_cast<const AvatarMixerClientData*>(sourceAvatarNode->getLinkedData());
assert(avatarClientNodeData); // we can't have gotten here without avatarNode having valid data assert(sourceAvatarNodeData); // we can't have gotten here without sourceAvatarNode having valid data
quint64 startIgnoreCalculation = usecTimestampNow(); quint64 startIgnoreCalculation = usecTimestampNow();
// make sure we have data for this avatar, that it isn't the same node, // 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 // and isn't an avatar that the viewing node has ignored
// or that has ignored the viewing node // or that has ignored the viewing node
if ((destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen) if ((destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) && !PALIsOpen)
|| (avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) { || (sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) {
shouldIgnore = true; sendAvatar = false;
} else { } else {
// Check to see if the space bubble is enabled // 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 // 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 // Perform the collision check between the two bounding boxes
AABox otherNodeBox = avatarClientNodeData->getAvatar().getDefaultBubbleBox(); AABox sourceNodeBox = sourceAvatarNodeData->getAvatar().getDefaultBubbleBox();
if (nodeBox.touches(otherNodeBox)) { if (destinationNodeBox.touches(sourceNodeBox)) {
nodeData->ignoreOther(destinationNode, avatarNode); destinationNodeData->ignoreOther(destinationNode, sourceAvatarNode);
shouldIgnore = !getsAnyIgnored; sendAvatar = getsAnyIgnored;
} }
} }
// Not close enough to ignore // Not close enough to ignore
if (!shouldIgnore) { if (sendAvatar) {
nodeData->removeFromRadiusIgnoringSet(avatarNode->getUUID()); destinationNodeData->removeFromRadiusIgnoringSet(sourceAvatarNode->getUUID());
} }
} }
if (!shouldIgnore) { if (sendAvatar) {
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getLocalID()); AvatarDataSequenceNumber lastSeqToReceiver = destinationNodeData->getLastBroadcastSequenceNumber(sourceAvatarNode->getLocalID());
AvatarDataSequenceNumber lastSeqFromSender = avatarClientNodeData->getLastReceivedSequenceNumber(); AvatarDataSequenceNumber lastSeqFromSender = sourceAvatarNodeData->getLastReceivedSequenceNumber();
// FIXME - This code does appear to be working. But it seems brittle. // FIXME - This code does appear to be working. But it seems brittle.
// It supports determining if the frame of data for this "other" // 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 // or that somehow we haven't sent
if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) { if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) {
++numAvatarsHeldBack; ++numAvatarsHeldBack;
shouldIgnore = true; sendAvatar = false;
} else if (lastSeqFromSender == 0) { } 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 // This is important for Agent scripts that are not avatar
// so that they don't appear to be an avatar at the origin // so that they don't appear to be an avatar at the origin
shouldIgnore = true; sendAvatar = false;
} else if (lastSeqFromSender - lastSeqToReceiver > 1) { } 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 // this is a skip - we still send the packet but capture the presence of the skip so we see it happening
++numAvatarsWithSkippedFrames; ++numAvatarsWithSkippedFrames;
} }
} }
quint64 endIgnoreCalculation = usecTimestampNow(); quint64 endIgnoreCalculation = usecTimestampNow();
_stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation); _stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation);
if (!shouldIgnore) { if (sendAvatar) {
// sort this one for later // sort this one for later
const AvatarData* avatarNodeData = avatarClientNodeData->getConstAvatarData(); const MixerAvatar* avatarNodeData = sourceAvatarNodeData->getConstAvatarData();
auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNode->getLocalID()); 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 // 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). // 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`. // However, it's less heavy-handed than using `shouldIgnore`.
if (PALWasOpen && !PALIsOpen && if (PALWasOpen && !PALIsOpen &&
(destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) || (destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) ||
avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) { sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) {
// ...send a Kill Packet to Node A, instructing Node A to kill Avatar B, // ...send a Kill Packet to Node A, instructing Node A to kill Avatar B,
// then have Node A cleanup the killed Node B. // then have Node A cleanup the killed Node B.
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true); 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); packet->writePrimitive(KillAvatarReason::AvatarIgnored);
nodeList->sendPacket(std::move(packet), *destinationNode); 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 // 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 traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData); auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
const int avatarPacketCapacity = avatarPacket->getPayloadCapacity(); const int avatarPacketCapacity = avatarPacket->getPayloadCapacity();
int avatarSpaceAvailable = avatarPacketCapacity; int avatarSpaceAvailable = avatarPacketCapacity;
int numPacketsSent = 0; int numPacketsSent = 0;
int numAvatarsSent = 0;
auto identityPacketList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); auto identityPacketList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst); // Loop over two priorities - hero avatars then everyone else:
for (const auto& sortedAvatar : sortedAvatarVector) { for (PriorityVariants currentVariant = kHero; currentVariant <= kNonhero; ++((int&)currentVariant)) {
const Node* otherNode = sortedAvatar.getNode(); const auto& sortedAvatarVector = avatarPriorityQueues[currentVariant].getSortedVector(numToSendEst);
auto lastEncodeForOther = sortedAvatar.getTimestamp(); 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, // 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. // or send minimal avatar data in uncommon case of PALIsOpen.
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars; int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes; auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes;
bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame; bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame;
if (overBudget) { if (overBudget) {
if (PALIsOpen) { if (PALIsOpen) {
_stats.overBudgetAvatars++; _stats.overBudgetAvatars++;
detail = AvatarData::PALMinimum; detail = AvatarData::PALMinimum;
} else { } else {
_stats.overBudgetAvatars += remainingAvatars; _stats.overBudgetAvatars += remainingAvatars;
break; 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) { if (destinationNodeData->getNumAvatarsSentLastFrame() > numToSendEst) {
qCWarning(avatars) << "More avatars sent than upper estimate" << nodeData->getNumAvatarsSentLastFrame() qCWarning(avatars) << "More avatars sent than upper estimate" << destinationNodeData->getNumAvatarsSentLastFrame()
<< " / " << numToSendEst; << " / " << numToSendEst;
} }
@ -618,12 +653,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
} }
// record the bytes sent for other avatar data in the AvatarMixerClientData // 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 // record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); destinationNodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); destinationNodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
quint64 endPacketSending = usecTimestampNow(); quint64 endPacketSending = usecTimestampNow();
_stats.packetSendingElapsedTime += (endPacketSending - startPacketSending); _stats.packetSendingElapsedTime += (endPacketSending - startPacketSending);

View file

@ -32,6 +32,7 @@ public:
int numIdentityPacketsSent { 0 }; int numIdentityPacketsSent { 0 };
int numOthersIncluded { 0 }; int numOthersIncluded { 0 };
int overBudgetAvatars { 0 }; int overBudgetAvatars { 0 };
int numHeroesIncluded { 0 };
quint64 ignoreCalculationElapsedTime { 0 }; quint64 ignoreCalculationElapsedTime { 0 };
quint64 avatarDataPackingElapsedTime { 0 }; quint64 avatarDataPackingElapsedTime { 0 };
@ -57,6 +58,7 @@ public:
numIdentityPacketsSent = 0; numIdentityPacketsSent = 0;
numOthersIncluded = 0; numOthersIncluded = 0;
overBudgetAvatars = 0; overBudgetAvatars = 0;
numHeroesIncluded = 0;
ignoreCalculationElapsedTime = 0; ignoreCalculationElapsedTime = 0;
avatarDataPackingElapsedTime = 0; avatarDataPackingElapsedTime = 0;
@ -80,6 +82,7 @@ public:
numIdentityPacketsSent += rhs.numIdentityPacketsSent; numIdentityPacketsSent += rhs.numIdentityPacketsSent;
numOthersIncluded += rhs.numOthersIncluded; numOthersIncluded += rhs.numOthersIncluded;
overBudgetAvatars += rhs.overBudgetAvatars; overBudgetAvatars += rhs.overBudgetAvatars;
numHeroesIncluded += rhs.numHeroesIncluded;
ignoreCalculationElapsedTime += rhs.ignoreCalculationElapsedTime; ignoreCalculationElapsedTime += rhs.ignoreCalculationElapsedTime;
avatarDataPackingElapsedTime += rhs.avatarDataPackingElapsedTime; avatarDataPackingElapsedTime += rhs.avatarDataPackingElapsedTime;
@ -90,9 +93,13 @@ public:
} }
}; };
class EntityTree;
using EntityTreePointer = std::shared_ptr<EntityTree>;
struct SlaveSharedData { struct SlaveSharedData {
QStringList skeletonURLWhitelist; QStringList skeletonURLWhitelist;
QUrl skeletonReplacementURL; QUrl skeletonReplacementURL;
EntityTreePointer entityTree;
}; };
class AvatarMixerSlave { class AvatarMixerSlave {

View file

@ -63,10 +63,6 @@ bool AvatarMixerSlaveThread::try_pop(SharedNodePointer& node) {
return _pool._queue.try_pop(node); return _pool._queue.try_pop(node);
} }
#ifdef AVATAR_SINGLE_THREADED
static AvatarMixerSlave slave;
#endif
void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) { void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) {
_function = &AvatarMixerSlave::processIncomingPackets; _function = &AvatarMixerSlave::processIncomingPackets;
_configure = [=](AvatarMixerSlave& slave) { _configure = [=](AvatarMixerSlave& slave) {
@ -89,19 +85,9 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) {
_begin = begin; _begin = begin;
_end = end; _end = end;
#ifdef AUDIO_SINGLE_THREADED
_configure(slave);
std::for_each(begin, end, [&](const SharedNodePointer& node) {
_function(slave, node);
});
#else
// fill the queue // fill the queue
std::for_each(_begin, _end, [&](const SharedNodePointer& node) { std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
#if defined(__clang__) && defined(Q_OS_LINUX)
_queue.push(node); _queue.push(node);
#else
_queue.emplace(node);
#endif
}); });
{ {
@ -121,18 +107,13 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) {
} }
assert(_queue.empty()); assert(_queue.empty());
#endif
} }
void AvatarMixerSlavePool::each(std::function<void(AvatarMixerSlave& slave)> functor) { void AvatarMixerSlavePool::each(std::function<void(AvatarMixerSlave& slave)> functor) {
#ifdef AVATAR_SINGLE_THREADED
functor(slave);
#else
for (auto& slave : _slaves) { for (auto& slave : _slaves) {
functor(*slave.get()); functor(*slave.get());
} }
#endif
} }
void AvatarMixerSlavePool::setNumThreads(int numThreads) { void AvatarMixerSlavePool::setNumThreads(int numThreads) {
@ -158,9 +139,6 @@ void AvatarMixerSlavePool::setNumThreads(int numThreads) {
void AvatarMixerSlavePool::resize(int numThreads) { void AvatarMixerSlavePool::resize(int numThreads) {
assert(_numThreads == (int)_slaves.size()); 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); qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads);
Lock lock(_mutex); Lock lock(_mutex);
@ -208,5 +186,4 @@ void AvatarMixerSlavePool::resize(int numThreads) {
_numThreads = _numStarted = _numFinished = numThreads; _numThreads = _numStarted = _numFinished = numThreads;
assert(_numThreads == (int)_slaves.size()); 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>(); auto nodeList = DependencyManager::get<NodeList>();
// we need to ask the DS about agents so we can ping/reply with them // 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 beforeRun(); // after payload has been processed

View file

@ -1302,6 +1302,14 @@
"placeholder": "1", "placeholder": "1",
"default": "1", "default": "1",
"advanced": true "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( limitedNodeList->eachMatchingNode(
[this, addedNode](const SharedNodePointer& node)->bool { [this, addedNode](const SharedNodePointer& node)->bool {
if (node->getLinkedData() && node->getActiveSocket() && node != addedNode) { // is the added Node in this node's interest list?
// is the added Node in this node's interest list? return node->getLinkedData()
return isInInterestSet(node, addedNode); && node->getActiveSocket()
} else { && node != addedNode
return false; && isInInterestSet(node, addedNode);
}
}, },
[this, &addNodePacket, connectionSecretIndex, addedNode, limitedNodeListWeak](const SharedNodePointer& node) { [this, &addNodePacket, connectionSecretIndex, addedNode, limitedNodeListWeak](const SharedNodePointer& node) {
// send off this packet to the node // send off this packet to the node

View file

@ -213,6 +213,63 @@ Item {
popup.open(); 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 { RalewayRegular {
id: infoMessage id: infoMessage
@ -240,7 +297,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: doctorStatusMessage.bottom
anchors.bottomMargin: 24 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." 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 { HifiControls.Button {
id: openFolderButton id: openFolderButton

View file

@ -77,7 +77,10 @@ public:
return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath())); return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath()));
} }
Q_INVOKABLE bool getHasErrors() const { return _hasErrors; } 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. * 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 // 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 // TODO: This is a horrible hack and once properties.constructFromBuffer no longer causes
// side effects...remove the following three lines // side effects...remove the following three lines
const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}");
entity->setParentID(NULL_ID); entity->setParentID(NULL_ID);
entity->setParentID(oldParentID); entity->setParentID(oldParentID);

View file

@ -1066,13 +1066,6 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
if (_enableInverseKinematics) { if (_enableInverseKinematics) {
_animVars.set("ikOverlayAlpha", 1.0f); _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 { } else {
_animVars.set("ikOverlayAlpha", 0.0f); _animVars.set("ikOverlayAlpha", 0.0f);
_animVars.set("splineIKEnabled", false); _animVars.set("splineIKEnabled", false);
@ -1086,6 +1079,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos
_animVars.set("rightFootPoleVectorEnabled", false); _animVars.set("rightFootPoleVectorEnabled", false);
} }
_lastEnableInverseKinematics = _enableInverseKinematics; _lastEnableInverseKinematics = _enableInverseKinematics;
} }
_lastForward = forward; _lastForward = forward;
_lastPosition = worldPosition; _lastPosition = worldPosition;

View file

@ -41,6 +41,7 @@
#include "EntitySimulation.h" #include "EntitySimulation.h"
#include "EntityDynamicFactoryInterface.h" #include "EntityDynamicFactoryInterface.h"
//#define WANT_DEBUG
Q_DECLARE_METATYPE(EntityItemPointer); Q_DECLARE_METATYPE(EntityItemPointer);
int entityItemPointernMetaTypeId = qRegisterMetaType<EntityItemPointer>(); int entityItemPointernMetaTypeId = qRegisterMetaType<EntityItemPointer>();
@ -95,6 +96,8 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param
requestedProperties += PROP_LAST_EDITED_BY; requestedProperties += PROP_LAST_EDITED_BY;
requestedProperties += PROP_ENTITY_HOST_TYPE; requestedProperties += PROP_ENTITY_HOST_TYPE;
requestedProperties += PROP_OWNING_AVATAR_ID; requestedProperties += PROP_OWNING_AVATAR_ID;
requestedProperties += PROP_PARENT_ID;
requestedProperties += PROP_PARENT_JOINT_INDEX;
requestedProperties += PROP_QUERY_AA_CUBE; requestedProperties += PROP_QUERY_AA_CUBE;
requestedProperties += PROP_CAN_CAST_SHADOW; requestedProperties += PROP_CAN_CAST_SHADOW;
requestedProperties += PROP_VISIBLE_IN_SECONDARY_CAMERA; requestedProperties += PROP_VISIBLE_IN_SECONDARY_CAMERA;
@ -502,6 +505,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
} }
#ifdef WANT_DEBUG #ifdef WANT_DEBUG
{
quint64 lastEdited = getLastEdited(); quint64 lastEdited = getLastEdited();
float editedAgo = getEditedAgo(); float editedAgo = getEditedAgo();
QString agoAsString = formatSecondsElapsed(editedAgo); 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) << " age=" << getAge() << "seconds - " << ageAsString;
qCDebug(entities) << " lastEdited =" << lastEdited; qCDebug(entities) << " lastEdited =" << lastEdited;
qCDebug(entities) << " ago=" << editedAgo << "seconds - " << agoAsString; qCDebug(entities) << " ago=" << editedAgo << "seconds - " << agoAsString;
}
#endif #endif
quint64 lastEditedFromBuffer = 0; quint64 lastEditedFromBuffer = 0;
@ -1099,7 +1104,7 @@ void EntityItem::simulate(const quint64& now) {
qCDebug(entities) << " hasGravity=" << hasGravity(); qCDebug(entities) << " hasGravity=" << hasGravity();
qCDebug(entities) << " hasAcceleration=" << hasAcceleration(); qCDebug(entities) << " hasAcceleration=" << hasAcceleration();
qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity();
qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity();
qCDebug(entities) << " isMortal=" << isMortal(); qCDebug(entities) << " isMortal=" << isMortal();
qCDebug(entities) << " getAge()=" << getAge(); qCDebug(entities) << " getAge()=" << getAge();
qCDebug(entities) << " getLifetime()=" << getLifetime(); qCDebug(entities) << " getLifetime()=" << getLifetime();
@ -1111,12 +1116,12 @@ void EntityItem::simulate(const quint64& now) {
qCDebug(entities) << " hasGravity=" << hasGravity(); qCDebug(entities) << " hasGravity=" << hasGravity();
qCDebug(entities) << " hasAcceleration=" << hasAcceleration(); qCDebug(entities) << " hasAcceleration=" << hasAcceleration();
qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity();
qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity();
} }
if (hasAngularVelocity()) { if (hasAngularVelocity()) {
qCDebug(entities) << " CHANGING...="; qCDebug(entities) << " CHANGING...=";
qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity();
qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity();
} }
if (isMortal()) { if (isMortal()) {
qCDebug(entities) << " MORTAL...="; 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 // the above cases not yet supported --> fall through to BOX case
case SHAPE_TYPE_BOX: { case SHAPE_TYPE_BOX: {
localPoint = glm::abs(localPoint); 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: { case SHAPE_TYPE_ELLIPSOID: {
// since we've transformed into the normalized space this is just a sphere-point intersection test // 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 // 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) // (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 // 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 SERVER_SCRIPTS_PROPERTY = "serverScripts";
static const QString ENTITY_TYPE_PROPERTY = "type";
if (jsonFilters[SERVER_SCRIPTS_PROPERTY] == EntityQueryFilterSymbol::NonDefault) { foreach(const auto& property, jsonFilters.keys()) {
return _serverScripts != ENTITY_ITEM_DEFAULT_SERVER_SCRIPTS; 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 // 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; } QUuid getLastEditedBy() const { return _lastEditedBy; }
void setLastEditedBy(QUuid value) { _lastEditedBy = value; } 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; } virtual bool getMeshes(MeshProxyList& result) { return true; }

View file

@ -225,6 +225,15 @@ QString EntityItemProperties::getBloomModeAsString() const {
return getComponentModeAsString(_bloomMode); 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) { 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 std::find_if(COMPONENT_MODES.begin(), COMPONENT_MODES.end(), [&](const ComponentPair& pair) {
return (pair.second == mode); 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 { QString EntityItemProperties::getKeyLightModeAsString() const {
return getComponentModeAsString(_keyLightMode); return getComponentModeAsString(_keyLightMode);
} }
@ -622,6 +640,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
CHECK_PROPERTY_CHANGE(PROP_SKYBOX_MODE, skyboxMode); CHECK_PROPERTY_CHANGE(PROP_SKYBOX_MODE, skyboxMode);
CHECK_PROPERTY_CHANGE(PROP_HAZE_MODE, hazeMode); CHECK_PROPERTY_CHANGE(PROP_HAZE_MODE, hazeMode);
CHECK_PROPERTY_CHANGE(PROP_BLOOM_MODE, bloomMode); CHECK_PROPERTY_CHANGE(PROP_BLOOM_MODE, bloomMode);
CHECK_PROPERTY_CHANGE(PROP_AVATAR_PRIORITY, avatarPriority);
// Polyvox // Polyvox
CHECK_PROPERTY_CHANGE(PROP_VOXEL_VOLUME_SIZE, voxelVolumeSize); 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 * @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 * 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 /> * 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> * <pre>
*
* function filter(properties) { * function filter(properties) {
* // Test and edit properties object values, * // Test and edit properties object values,
* // e.g., properties.modelURL, as required. * // 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_SKYBOX_MODE, skyboxMode, getSkyboxModeAsString());
COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_HAZE_MODE, hazeMode, getHazeModeAsString()); 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_BLOOM_MODE, bloomMode, getBloomModeAsString());
COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_AVATAR_PRIORITY, avatarPriority, getAvatarPriorityAsString());
} }
// Web only // 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(skyboxMode, SkyboxMode);
COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(hazeMode, HazeMode); COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(hazeMode, HazeMode);
COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(bloomMode, BloomMode); COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(bloomMode, BloomMode);
COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(avatarPriority, AvatarPriority);
// Polyvox // Polyvox
COPY_PROPERTY_FROM_QSCRIPTVALUE(voxelVolumeSize, vec3, setVoxelVolumeSize); 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(skyboxMode);
COPY_PROPERTY_IF_CHANGED(hazeMode); COPY_PROPERTY_IF_CHANGED(hazeMode);
COPY_PROPERTY_IF_CHANGED(bloomMode); COPY_PROPERTY_IF_CHANGED(bloomMode);
COPY_PROPERTY_IF_CHANGED(avatarPriority);
// Polyvox // Polyvox
COPY_PROPERTY_IF_CHANGED(voxelVolumeSize); 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_SKYBOX_MODE, SkyboxMode, skyboxMode, uint32_t);
ADD_PROPERTY_TO_MAP(PROP_HAZE_MODE, HazeMode, hazeMode, 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_BLOOM_MODE, BloomMode, bloomMode, uint32_t);
ADD_PROPERTY_TO_MAP(PROP_AVATAR_PRIORITY, AvatarPriority, avatarPriority, uint32_t);
// Polyvox // Polyvox
ADD_PROPERTY_TO_MAP(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, vec3); 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_SKYBOX_MODE, (uint32_t)properties.getSkyboxMode());
APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)properties.getHazeMode()); APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)properties.getHazeMode());
APPEND_ENTITY_PROPERTY(PROP_BLOOM_MODE, (uint32_t)properties.getBloomMode()); 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) { 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_SKYBOX_MODE, uint32_t, setSkyboxMode);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_HAZE_MODE, uint32_t, setHazeMode); 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_BLOOM_MODE, uint32_t, setBloomMode);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_AVATAR_PRIORITY, uint32_t, setAvatarPriority);
} }
if (properties.getType() == EntityTypes::PolyVox) { if (properties.getType() == EntityTypes::PolyVox) {
@ -4039,6 +4070,7 @@ void EntityItemProperties::markAllChanged() {
_skyboxModeChanged = true; _skyboxModeChanged = true;
_hazeModeChanged = true; _hazeModeChanged = true;
_bloomModeChanged = true; _bloomModeChanged = true;
_avatarPriorityChanged = true;
// Polyvox // Polyvox
_voxelVolumeSizeChanged = true; _voxelVolumeSizeChanged = true;
@ -4637,6 +4669,9 @@ QList<QString> EntityItemProperties::listChangedProperties() {
if (bloomModeChanged()) { if (bloomModeChanged()) {
out += "bloomMode"; out += "bloomMode";
} }
if (avatarPriorityChanged()) {
out += "avatarPriority";
}
// Polyvox // Polyvox
if (voxelVolumeSizeChanged()) { 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_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_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_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 // Polyvox
DEFINE_PROPERTY_REF(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3, PolyVoxEntityItem::DEFAULT_VOXEL_VOLUME_SIZE); 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, GhostingAllowed, ghostingAllowed, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, FilterURL, filterURL, ""); 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, EntityHostTypeAsString, entityHostType, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, "");

View file

@ -156,6 +156,7 @@ enum EntityPropertyList {
PROP_DERIVED_28, PROP_DERIVED_28,
PROP_DERIVED_29, PROP_DERIVED_29,
PROP_DERIVED_30, PROP_DERIVED_30,
PROP_DERIVED_31,
PROP_AFTER_LAST_ITEM, PROP_AFTER_LAST_ITEM,
@ -276,6 +277,8 @@ enum EntityPropertyList {
PROP_SKYBOX_MODE = PROP_DERIVED_28, PROP_SKYBOX_MODE = PROP_DERIVED_28,
PROP_HAZE_MODE = PROP_DERIVED_29, PROP_HAZE_MODE = PROP_DERIVED_29,
PROP_BLOOM_MODE = PROP_DERIVED_30, PROP_BLOOM_MODE = PROP_DERIVED_30,
// Avatar priority
PROP_AVATAR_PRIORITY = PROP_DERIVED_31,
// Polyvox // Polyvox
PROP_VOXEL_VOLUME_SIZE = PROP_DERIVED_0, 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(skyboxMode, getSkyboxMode);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(hazeMode, getHazeMode); COPY_ENTITY_PROPERTY_TO_PROPERTIES(hazeMode, getHazeMode);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(bloomMode, getBloomMode); COPY_ENTITY_PROPERTY_TO_PROPERTIES(bloomMode, getBloomMode);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(avatarPriority, getAvatarPriority);
return properties; return properties;
} }
@ -117,6 +118,7 @@ bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& propertie
SET_ENTITY_PROPERTY_FROM_PROPERTIES(skyboxMode, setSkyboxMode); SET_ENTITY_PROPERTY_FROM_PROPERTIES(skyboxMode, setSkyboxMode);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(hazeMode, setHazeMode); SET_ENTITY_PROPERTY_FROM_PROPERTIES(hazeMode, setHazeMode);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(bloomMode, setBloomMode); SET_ENTITY_PROPERTY_FROM_PROPERTIES(bloomMode, setBloomMode);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(avatarPriority, setAvatarPriority);
somethingChanged = somethingChanged || _keyLightPropertiesChanged || _ambientLightPropertiesChanged || somethingChanged = somethingChanged || _keyLightPropertiesChanged || _ambientLightPropertiesChanged ||
_skyboxPropertiesChanged || _hazePropertiesChanged || _bloomPropertiesChanged; _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_SKYBOX_MODE, uint32_t, setSkyboxMode);
READ_ENTITY_PROPERTY(PROP_HAZE_MODE, uint32_t, setHazeMode); READ_ENTITY_PROPERTY(PROP_HAZE_MODE, uint32_t, setHazeMode);
READ_ENTITY_PROPERTY(PROP_BLOOM_MODE, uint32_t, setBloomMode); READ_ENTITY_PROPERTY(PROP_BLOOM_MODE, uint32_t, setBloomMode);
READ_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, uint32_t, setAvatarPriority);
return bytesRead; return bytesRead;
} }
@ -211,6 +214,7 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p
requestedProperties += PROP_FLYING_ALLOWED; requestedProperties += PROP_FLYING_ALLOWED;
requestedProperties += PROP_GHOSTING_ALLOWED; requestedProperties += PROP_GHOSTING_ALLOWED;
requestedProperties += PROP_FILTER_URL; requestedProperties += PROP_FILTER_URL;
requestedProperties += PROP_AVATAR_PRIORITY;
requestedProperties += PROP_KEY_LIGHT_MODE; requestedProperties += PROP_KEY_LIGHT_MODE;
requestedProperties += PROP_AMBIENT_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_SKYBOX_MODE, (uint32_t)getSkyboxMode());
APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)getHazeMode()); APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)getHazeMode());
APPEND_ENTITY_PROPERTY(PROP_BLOOM_MODE, (uint32_t)getBloomMode()); APPEND_ENTITY_PROPERTY(PROP_BLOOM_MODE, (uint32_t)getBloomMode());
APPEND_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, getAvatarPriority());
} }
void ZoneEntityItem::debugDump() const { void ZoneEntityItem::debugDump() const {
@ -269,6 +274,7 @@ void ZoneEntityItem::debugDump() const {
qCDebug(entities) << " _ambientLightMode:" << EntityItemProperties::getComponentModeAsString(_ambientLightMode); qCDebug(entities) << " _ambientLightMode:" << EntityItemProperties::getComponentModeAsString(_ambientLightMode);
qCDebug(entities) << " _skyboxMode:" << EntityItemProperties::getComponentModeAsString(_skyboxMode); qCDebug(entities) << " _skyboxMode:" << EntityItemProperties::getComponentModeAsString(_skyboxMode);
qCDebug(entities) << " _bloomMode:" << EntityItemProperties::getComponentModeAsString(_bloomMode); qCDebug(entities) << " _bloomMode:" << EntityItemProperties::getComponentModeAsString(_bloomMode);
qCDebug(entities) << " _avatarPriority:" << getAvatarPriority();
_keyLightProperties.debugDump(); _keyLightProperties.debugDump();
_ambientLightProperties.debugDump(); _ambientLightProperties.debugDump();
@ -463,3 +469,18 @@ void ZoneEntityItem::fetchCollisionGeometryResource() {
_shapeResource = DependencyManager::get<ModelCache>()->getCollisionGeometryResource(hullURL); _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; QString getCompoundShapeURL() const;
virtual void setCompoundShapeURL(const QString& url); virtual void setCompoundShapeURL(const QString& url);
virtual bool matchesJSONFilters(const QJsonObject& jsonFilters) const override;
KeyLightPropertyGroup getKeyLightProperties() const { return resultWithReadLock<KeyLightPropertyGroup>([&] { return _keyLightProperties; }); } KeyLightPropertyGroup getKeyLightProperties() const { return resultWithReadLock<KeyLightPropertyGroup>([&] { return _keyLightProperties; }); }
AmbientLightPropertyGroup getAmbientLightProperties() const { return resultWithReadLock<AmbientLightPropertyGroup>([&] { return _ambientLightProperties; }); } AmbientLightPropertyGroup getAmbientLightProperties() const { return resultWithReadLock<AmbientLightPropertyGroup>([&] { return _ambientLightProperties; }); }
@ -96,6 +98,9 @@ public:
QString getFilterURL() const; QString getFilterURL() const;
void setFilterURL(const QString url); void setFilterURL(const QString url);
uint32_t getAvatarPriority() const { return _avatarPriority; }
void setAvatarPriority(uint32_t value) { _avatarPriority = value; }
bool keyLightPropertiesChanged() const { return _keyLightPropertiesChanged; } bool keyLightPropertiesChanged() const { return _keyLightPropertiesChanged; }
bool ambientLightPropertiesChanged() const { return _ambientLightPropertiesChanged; } bool ambientLightPropertiesChanged() const { return _ambientLightPropertiesChanged; }
bool skyboxPropertiesChanged() const { return _skyboxPropertiesChanged; } bool skyboxPropertiesChanged() const { return _skyboxPropertiesChanged; }
@ -147,6 +152,9 @@ protected:
bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED }; bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED };
QString _filterURL { DEFAULT_FILTER_URL }; 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. // Dirty flags turn true when either keylight properties is changing values.
bool _keyLightPropertiesChanged { false }; bool _keyLightPropertiesChanged { false };
bool _ambientLightPropertiesChanged { false }; bool _ambientLightPropertiesChanged { false };

View file

@ -167,7 +167,6 @@ glm::mat4 getGlobalTransform(const QMultiMap<QString, QString>& _connectionParen
} }
} }
} }
return globalTransform; return globalTransform;
} }
@ -436,6 +435,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
hfmModel.originalURL = url; hfmModel.originalURL = url;
float unitScaleFactor = 1.0f; float unitScaleFactor = 1.0f;
glm::quat upAxisZRotation;
bool applyUpAxisZRotation = false;
glm::vec3 ambientColor; glm::vec3 ambientColor;
QString hifiGlobalNodeID; QString hifiGlobalNodeID;
unsigned int meshIndex = 0; unsigned int meshIndex = 0;
@ -473,11 +474,22 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
if (subobject.name == propertyName) { if (subobject.name == propertyName) {
static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor"); static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor");
static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor"); static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor");
static const QVariant UP_AXIS = QByteArray("UpAxis");
const auto& subpropName = subobject.properties.at(0); const auto& subpropName = subobject.properties.at(0);
if (subpropName == UNIT_SCALE_FACTOR) { if (subpropName == UNIT_SCALE_FACTOR) {
unitScaleFactor = subobject.properties.at(index).toFloat(); unitScaleFactor = subobject.properties.at(index).toFloat();
} else if (subpropName == AMBIENT_COLOR) { } else if (subpropName == AMBIENT_COLOR) {
ambientColor = getVec3(subobject.properties, index); ambientColor = getVec3(subobject.properties, index);
} else if (subpropName == UP_AXIS) {
constexpr int UP_AXIS_Y = 1;
constexpr int UP_AXIS_Z = 2;
int upAxis = subobject.properties.at(index).toInt();
if (upAxis == UP_AXIS_Y) {
// No update necessary, y up is the default
} else if (upAxis == UP_AXIS_Z) {
upAxisZRotation = glm::angleAxis(glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
applyUpAxisZRotation = true;
}
} }
} }
} }
@ -1269,9 +1281,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
joint.geometricScaling = fbxModel.geometricScaling; joint.geometricScaling = fbxModel.geometricScaling;
joint.isSkeletonJoint = fbxModel.isLimbNode; joint.isSkeletonJoint = fbxModel.isLimbNode;
hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint); hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint);
if (applyUpAxisZRotation && joint.parentIndex == -1) {
joint.rotation *= upAxisZRotation;
joint.translation = upAxisZRotation * joint.translation;
}
glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation;
if (joint.parentIndex == -1) { if (joint.parentIndex == -1) {
joint.transform = hfmModel.offset * glm::translate(joint.translation) * joint.preTransform * joint.transform = hfmModel.offset * glm::translate(joint.translation) * joint.preTransform *
glm::mat4_cast(combinedRotation) * joint.postTransform; glm::mat4_cast(combinedRotation) * joint.postTransform;
@ -1664,6 +1678,14 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
} }
} }
if (applyUpAxisZRotation) {
hfmModelPtr->meshExtents.transform(glm::mat4_cast(upAxisZRotation));
hfmModelPtr->bindExtents.transform(glm::mat4_cast(upAxisZRotation));
for (auto &mesh : hfmModelPtr->meshes) {
mesh.modelTransform *= glm::mat4_cast(upAxisZRotation);
mesh.meshExtents.transform(glm::mat4_cast(upAxisZRotation));
}
}
return hfmModelPtr; return hfmModelPtr;
} }

View file

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

View file

@ -17,13 +17,14 @@
#include <hfm/HFM.h> #include <hfm/HFM.h>
#include "Engine.h" #include "Engine.h"
#include "BakerTypes.h"
#include "ParseMaterialMappingTask.h" #include "ParseMaterialMappingTask.h"
namespace baker { namespace baker {
class Baker { class Baker {
public: public:
Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping); Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping);
void run(); void run();

View file

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

View file

@ -10,7 +10,9 @@
#include "ModelBakerLogging.h" #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; MaterialMapping materialMapping;
auto mappingIter = mapping.find("materialMap"); 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(); }); NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread()); materialResource->moveToThread(qApp->thread());
// TODO: add baseURL to allow FSTs to reference relative files next to them materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url);
materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), QUrl());
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), materialResource)); materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), materialResource));
} }
} else if (mappingJSON.isString()) { } else if (mappingJSON.isString()) {
auto mappingValue = mappingJSON.toString(); 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 <hfm/HFM.h>
#include "Engine.h" #include "Engine.h"
#include "BakerTypes.h"
#include <material-networking/MaterialCache.h> #include <material-networking/MaterialCache.h>
class ParseMaterialMappingTask { class ParseMaterialMappingTask {
public: public:
using Input = QVariantHash; using Input = baker::GeometryMappingPair;
using Output = MaterialMapping; using Output = MaterialMapping;
using JobModel = baker::Job::ModelIO<ParseMaterialMappingTask, Input, Output>; 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(); auto& jointIndices = output.edit2();
// Get joint renames // Get joint renames
auto jointNameMapping = getJointNameMapping(mapping); auto jointNameMapping = getJointNameMapping(mapping.second);
// Apply joint metadata from FST file mappings // Apply joint metadata from FST file mappings
for (const auto& jointIn : jointsIn) { for (const auto& jointIn : jointsIn) {
jointsOut.push_back(jointIn); 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 // 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++) { for (auto itr = offsets.begin(); itr != offsets.end(); itr++) {
QString jointName = itr.key(); QString jointName = itr.key();
int jointIndex = jointIndices.value(jointName) - 1; int jointIndex = jointIndices.value(jointName) - 1;

View file

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

View file

@ -35,11 +35,13 @@ class GeometryReader;
class GeometryExtra { class GeometryExtra {
public: public:
const QVariantHash& mapping; const GeometryMappingPair& mapping;
const QUrl& textureBaseUrl; const QUrl& textureBaseUrl;
bool combineParts; bool combineParts;
}; };
int geometryMappingPairTypeId = qRegisterMetaType<GeometryMappingPair>("GeometryMappingPair");
// From: https://stackoverflow.com/questions/41145012/how-to-hash-qvariant // From: https://stackoverflow.com/questions/41145012/how-to-hash-qvariant
class QVariantHasher { class QVariantHasher {
public: public:
@ -78,7 +80,8 @@ namespace std {
struct hash<GeometryExtra> { struct hash<GeometryExtra> {
size_t operator()(const GeometryExtra& geometryExtra) const { size_t operator()(const GeometryExtra& geometryExtra) const {
size_t result = 0; 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; return result;
} }
}; };
@ -151,7 +154,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) {
} }
auto modelCache = DependencyManager::get<ModelCache>(); auto modelCache = DependencyManager::get<ModelCache>();
GeometryExtra extra { _mapping, _textureBaseUrl, false }; GeometryExtra extra { GeometryMappingPair(_url, _mapping), _textureBaseUrl, false };
// Get the raw GeometryResource // Get the raw GeometryResource
_geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash<GeometryExtra>()(extra)).staticCast<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 { class GeometryReader : public QRunnable {
public: 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) : const QByteArray& data, bool combineParts, const QString& webMediaType) :
_modelLoader(modelLoader), _resource(resource), _url(url), _mapping(mapping), _data(data), _combineParts(combineParts), _webMediaType(webMediaType) { _modelLoader(modelLoader), _resource(resource), _url(url), _mapping(mapping), _data(data), _combineParts(combineParts), _webMediaType(webMediaType) {
@ -204,7 +207,7 @@ private:
ModelLoader _modelLoader; ModelLoader _modelLoader;
QWeakPointer<Resource> _resource; QWeakPointer<Resource> _resource;
QUrl _url; QUrl _url;
QVariantHash _mapping; GeometryMappingPair _mapping;
QByteArray _data; QByteArray _data;
bool _combineParts; bool _combineParts;
QString _webMediaType; QString _webMediaType;
@ -244,7 +247,7 @@ void GeometryReader::run() {
} }
HFMModel::Pointer hfmModel; HFMModel::Pointer hfmModel;
QVariantHash serializerMapping = _mapping; QVariantHash serializerMapping = _mapping.second;
serializerMapping["combineParts"] = _combineParts; serializerMapping["combineParts"] = _combineParts;
if (_url.path().toLower().endsWith(".gz")) { if (_url.path().toLower().endsWith(".gz")) {
@ -270,15 +273,14 @@ void GeometryReader::run() {
} }
// Add scripts to hfmModel // Add scripts to hfmModel
if (!_mapping.value(SCRIPT_FIELD).isNull()) { if (!serializerMapping.value(SCRIPT_FIELD).isNull()) {
QVariantList scripts = _mapping.values(SCRIPT_FIELD); QVariantList scripts = serializerMapping.values(SCRIPT_FIELD);
for (auto &script : scripts) { for (auto &script : scripts) {
hfmModel->scripts.push_back(script.toString()); hfmModel->scripts.push_back(script.toString());
} }
} }
QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", 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&) { } catch (const std::exception&) {
auto resource = _resource.toStrongRef(); auto resource = _resource.toStrongRef();
if (resource) { if (resource) {
@ -312,17 +314,17 @@ public:
void setExtra(void* extra) override; void setExtra(void* extra) override;
protected: protected:
Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping); Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping);
private: private:
ModelLoader _modelLoader; ModelLoader _modelLoader;
QVariantHash _mapping; GeometryMappingPair _mapping;
bool _combineParts; bool _combineParts;
}; };
void GeometryDefinitionResource::setExtra(void* extra) { void GeometryDefinitionResource::setExtra(void* extra) {
const GeometryExtra* geometryExtra = static_cast<const GeometryExtra*>(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(); _textureBaseUrl = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl();
_combineParts = geometryExtra ? geometryExtra->combineParts : true; _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())); 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 // Do processing on the model
baker::Baker modelBaker(hfmModel, mapping); baker::Baker modelBaker(hfmModel, mapping);
modelBaker.run(); modelBaker.run();
@ -394,11 +396,15 @@ QSharedPointer<Resource> ModelCache::createResource(const QUrl& url) {
} }
QSharedPointer<Resource> ModelCache::createResourceCopy(const QSharedPointer<Resource>& resource) { 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, GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url,
const QVariantHash& mapping, const QUrl& textureBaseUrl) { const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) {
bool combineParts = true; bool combineParts = true;
GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts }; GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash<GeometryExtra>()(geometryExtra)).staticCast<GeometryResource>(); 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, GeometryResource::Pointer ModelCache::getCollisionGeometryResource(const QUrl& url,
const QVariantHash& mapping, const QUrl& textureBaseUrl) { const GeometryMappingPair& mapping,
const QUrl& textureBaseUrl) {
bool combineParts = false; bool combineParts = false;
GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts }; GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash<GeometryExtra>()(geometryExtra)).staticCast<GeometryResource>(); GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash<GeometryExtra>()(geometryExtra)).staticCast<GeometryResource>();

View file

@ -26,6 +26,9 @@ class MeshPart;
class GeometryMappingResource; class GeometryMappingResource;
using GeometryMappingPair = std::pair<QUrl, QVariantHash>;
Q_DECLARE_METATYPE(GeometryMappingPair)
class Geometry { class Geometry {
public: public:
using Pointer = std::shared_ptr<Geometry>; using Pointer = std::shared_ptr<Geometry>;
@ -145,11 +148,13 @@ class ModelCache : public ResourceCache, public Dependency {
public: public:
GeometryResource::Pointer getGeometryResource(const QUrl& url, GeometryResource::Pointer getGeometryResource(const QUrl& url,
const QVariantHash& mapping = QVariantHash(), const GeometryMappingPair& mapping =
GeometryMappingPair(QUrl(), QVariantHash()),
const QUrl& textureBaseUrl = QUrl()); const QUrl& textureBaseUrl = QUrl());
GeometryResource::Pointer getCollisionGeometryResource(const QUrl& url, GeometryResource::Pointer getCollisionGeometryResource(const QUrl& url,
const QVariantHash& mapping = QVariantHash(), const GeometryMappingPair& mapping =
GeometryMappingPair(QUrl(), QVariantHash()),
const QUrl& textureBaseUrl = QUrl()); const QUrl& textureBaseUrl = QUrl());
protected: protected:

View file

@ -40,6 +40,9 @@
static Setting::Handle<quint16> LIMITED_NODELIST_LOCAL_PORT("LimitedNodeList.LocalPort", 0); 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 = { const std::set<NodeType_t> SOLO_NODE_TYPES = {
NodeType::AvatarMixer, NodeType::AvatarMixer,
NodeType::AudioMixer, NodeType::AudioMixer,
@ -88,6 +91,11 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) :
connect(statsSampleTimer, &QTimer::timeout, this, &LimitedNodeList::sampleConnectionStats); connect(statsSampleTimer, &QTimer::timeout, this, &LimitedNodeList::sampleConnectionStats);
statsSampleTimer->start(CONNECTION_STATS_SAMPLE_INTERVAL_MSECS); 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 // check the local socket right now
updateLocalSocket(); updateLocalSocket();
@ -367,7 +375,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
return true; return true;
} else { } else if (!isDelayedNode(sourceID)){
HIFI_FCDEBUG(networking(), HIFI_FCDEBUG(networking(),
"Packet of type" << headerType << "received from unknown node with Local ID" << sourceLocalID); "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() { void LimitedNodeList::eraseAllNodes() {
QSet<SharedNodePointer> killedNodes; std::vector<SharedNodePointer> killedNodes;
{ {
// iterate the current nodes - grab them so we can emit that they are dying // iterate the current nodes - grab them so we can emit that they are dying
// and then remove them from the hash // and then remove them from the hash
QWriteLocker writeLocker(&_nodeMutex); QWriteLocker writeLocker(&_nodeMutex);
_localIDMap.clear();
if (_nodeHash.size() > 0) { if (_nodeHash.size() > 0) {
qCDebug(networking) << "LimitedNodeList::eraseAllNodes() removing all nodes from NodeList."; qCDebug(networking) << "LimitedNodeList::eraseAllNodes() removing all nodes from NodeList.";
auto it = _nodeHash.begin(); killedNodes.reserve(_nodeHash.size());
for (auto& pair : _nodeHash) {
while (it != _nodeHash.end()) { killedNodes.push_back(pair.second);
killedNodes.insert(it->second);
it = _nodeHash.unsafe_erase(it);
} }
} }
_localIDMap.clear();
_nodeHash.clear();
} }
foreach(const SharedNodePointer& killedNode, killedNodes) { foreach(const SharedNodePointer& killedNode, killedNodes) {
@ -593,18 +599,13 @@ void LimitedNodeList::reset() {
} }
bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID, ConnectionID newConnectionID) { bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID, ConnectionID newConnectionID) {
QReadLocker readLocker(&_nodeMutex); auto matchingNode = nodeWithUUID(nodeUUID);
NodeHash::iterator it = _nodeHash.find(nodeUUID);
if (it != _nodeHash.end()) {
SharedNodePointer matchingNode = it->second;
readLocker.unlock();
if (matchingNode) {
{ {
QWriteLocker writeLocker(&_nodeMutex); QWriteLocker writeLocker(&_nodeMutex);
_localIDMap.unsafe_erase(matchingNode->getLocalID()); _localIDMap.unsafe_erase(matchingNode->getLocalID());
_nodeHash.unsafe_erase(it); _nodeHash.unsafe_erase(matchingNode->getUUID());
} }
handleNodeKill(matchingNode, newConnectionID); handleNodeKill(matchingNode, newConnectionID);
@ -645,30 +646,26 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
Node::LocalID localID, bool isReplicated, bool isUpstream, Node::LocalID localID, bool isReplicated, bool isUpstream,
const QUuid& connectionSecret, const NodePermissions& permissions) { const QUuid& connectionSecret, const NodePermissions& permissions) {
{ auto matchingNode = nodeWithUUID(uuid);
QReadLocker readLocker(&_nodeMutex); if (matchingNode) {
NodeHash::const_iterator it = _nodeHash.find(uuid); 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()) { return matchingNode;
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;
}
} }
auto removeOldNode = [&](auto node) { auto removeOldNode = [&](auto node) {
if (node) { if (node) {
QWriteLocker writeLocker(&_nodeMutex); {
_localIDMap.unsafe_erase(node->getLocalID()); QWriteLocker writeLocker(&_nodeMutex);
_nodeHash.unsafe_erase(node->getUUID()); _localIDMap.unsafe_erase(node->getLocalID());
_nodeHash.unsafe_erase(node->getUUID());
}
handleNodeKill(node); handleNodeKill(node);
} }
}; };
@ -736,6 +733,53 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
return newNodePointer; 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) { std::unique_ptr<NLPacket> LimitedNodeList::constructPingPacket(const QUuid& nodeId, PingType_t pingType) {
int packetSize = sizeof(PingType_t) + sizeof(quint64) + sizeof(int64_t); 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){ eachNode([&](const SharedNodePointer& node){
if (node && destinationNodeTypes.contains(node->getType())) { if (node && destinationNodeTypes.contains(node->getType())) {
if (packet->isReliable()) { if (packet->isReliable()) {
auto packetCopy = NLPacket::createCopy(*packet); auto packetCopy = NLPacket::createCopy(*packet);
sendPacket(std::move(packetCopy), *node); sendPacket(std::move(packetCopy), *node);
} else { } else {
sendUnreliablePacket(*packet, *node); sendUnreliablePacket(*packet, *node);
} }
++n; ++n;
} }
}); });

View file

@ -51,6 +51,8 @@ const int INVALID_PORT = -1;
const quint64 NODE_SILENCE_THRESHOLD_MSECS = 5 * 1000; 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; extern const std::set<NodeType_t> SOLO_NODE_TYPES;
const char DEFAULT_ASSIGNMENT_SERVER_HOSTNAME[] = "localhost"; const char DEFAULT_ASSIGNMENT_SERVER_HOSTNAME[] = "localhost";
@ -205,7 +207,10 @@ public:
int* lockWaitOut = nullptr, int* lockWaitOut = nullptr,
int* nodeTransformOut = nullptr, int* nodeTransformOut = nullptr,
int* functorOut = nullptr) { int* functorOut = nullptr) {
auto start = usecTimestampNow(); quint64 start, endTransform, endFunctor;
start = usecTimestampNow();
std::vector<SharedNodePointer> nodes;
{ {
QReadLocker readLock(&_nodeMutex); QReadLocker readLock(&_nodeMutex);
auto endLock = usecTimestampNow(); auto endLock = usecTimestampNow();
@ -216,21 +221,21 @@ public:
// Size of _nodeHash could change at any time, // Size of _nodeHash could change at any time,
// so reserve enough memory for the current size // so reserve enough memory for the current size
// and then back insert all the nodes found // and then back insert all the nodes found
std::vector<SharedNodePointer> nodes;
nodes.reserve(_nodeHash.size()); nodes.reserve(_nodeHash.size());
std::transform(_nodeHash.cbegin(), _nodeHash.cend(), std::back_inserter(nodes), [&](const NodeHash::value_type& it) { std::transform(_nodeHash.cbegin(), _nodeHash.cend(), std::back_inserter(nodes), [&](const NodeHash::value_type& it) {
return it.second; return it.second;
}); });
auto endTransform = usecTimestampNow();
endTransform = usecTimestampNow();
if (nodeTransformOut) { if (nodeTransformOut) {
*nodeTransformOut = (endTransform - endLock); *nodeTransformOut = (endTransform - endLock);
} }
}
functor(nodes.cbegin(), nodes.cend()); functor(nodes.cbegin(), nodes.cend());
auto endFunctor = usecTimestampNow(); endFunctor = usecTimestampNow();
if (functorOut) { if (functorOut) {
*functorOut = (endFunctor - endTransform); *functorOut = (endFunctor - endTransform);
}
} }
} }
@ -316,6 +321,9 @@ public:
void sendFakedHandshakeRequestToNode(SharedNodePointer node); void sendFakedHandshakeRequestToNode(SharedNodePointer node);
#endif #endif
size_t getMaxConnectionRate() const { return _maxConnectionRate; }
void setMaxConnectionRate(size_t rate) { _maxConnectionRate = rate; }
int getInboundPPS() const { return _inboundPPS; } int getInboundPPS() const { return _inboundPPS; }
int getOutboundPPS() const { return _outboundPPS; } int getOutboundPPS() const { return _outboundPPS; }
float getInboundKbps() const { return _inboundKbps; } float getInboundKbps() const { return _inboundKbps; }
@ -367,7 +375,20 @@ protected slots:
void clientConnectionToSockAddrReset(const HifiSockAddr& sockAddr); void clientConnectionToSockAddrReset(const HifiSockAddr& sockAddr);
void processDelayedAdds();
protected: 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(int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT);
LimitedNodeList(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton 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 void operator=(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton
@ -390,6 +411,11 @@ protected:
bool sockAddrBelongsToNode(const HifiSockAddr& sockAddr); bool sockAddrBelongsToNode(const HifiSockAddr& sockAddr);
void addNewNode(NewNodeInfo info);
void delayNodeAdd(NewNodeInfo info);
void removeDelayedAdd(QUuid nodeUUID);
bool isDelayedNode(QUuid nodeUUID);
NodeHash _nodeHash; NodeHash _nodeHash;
mutable QReadWriteLock _nodeMutex { QReadWriteLock::Recursive }; mutable QReadWriteLock _nodeMutex { QReadWriteLock::Recursive };
udt::Socket _nodeSocket; udt::Socket _nodeSocket;
@ -440,6 +466,10 @@ private:
Node::LocalID _sessionLocalID { 0 }; Node::LocalID _sessionLocalID { 0 };
bool _flagTimeForConnectionStep { false }; // only keep track in interface 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 _inboundPPS { 0 };
int _outboundPPS { 0 }; int _outboundPPS { 0 };
float _inboundKbps { 0.0f }; 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) { void NodeList::processPingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
// send back a reply // send back a reply
auto replyPacket = constructPingReplyPacket(*message); auto replyPacket = constructPingReplyPacket(*message);
const HifiSockAddr& senderSockAddr = message->getSenderSockAddr(); const HifiSockAddr& senderSockAddr = message->getSenderSockAddr();
@ -291,41 +290,47 @@ void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes)
void NodeList::sendDomainServerCheckIn() { 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) { if (!_sendDomainServerCheckInEnabled) {
qCDebug(networking) << "Refusing to send a domain-server check in while it is disabled."; qCDebug(networking) << "Refusing to send a domain-server check in while it is disabled.";
return; return;
} }
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "sendDomainServerCheckIn", Qt::QueuedConnection);
return;
}
if (_isShuttingDown) { if (_isShuttingDown) {
qCDebug(networking) << "Refusing to send a domain-server check in while shutting down."; qCDebug(networking) << "Refusing to send a domain-server check in while shutting down.";
return; 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 // 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."; 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."; qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in.";
handleICEConnectionToDomainServer(); handleICEConnectionToDomainServer();
// let the domain handler know we are due to send a checkin packet // let the domain handler know we are due to send a checkin packet
} else if (!_domainHandler.getIP().isNull() && !_domainHandler.checkInPacketTimeout()) { } else if (!domainHandlerIp.isNull() && !_domainHandler.checkInPacketTimeout()) {
bool domainIsConnected = _domainHandler.isConnected();
PacketType domainPacketType = !_domainHandler.isConnected() HifiSockAddr domainSockAddr = _domainHandler.getSockAddr();
PacketType domainPacketType = !domainIsConnected
? PacketType::DomainConnectRequest : PacketType::DomainListRequest; ? PacketType::DomainConnectRequest : PacketType::DomainListRequest;
if (!_domainHandler.isConnected()) { if (!domainIsConnected) {
qCDebug(networking) << "Sending connect request to domain-server at" << _domainHandler.getHostname(); auto hostname = _domainHandler.getHostname();
qCDebug(networking) << "Sending connect request to domain-server at" << hostname;
// is this our localhost domain-server? // 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 so we need to make sure we have an up-to-date local port in case it restarted
if (_domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost if (domainSockAddr.getAddress() == QHostAddress::LocalHost
|| _domainHandler.getHostname() == "localhost") { || hostname == "localhost") {
quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT; quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT;
getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, domainPort); getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, domainPort);
@ -338,7 +343,7 @@ void NodeList::sendDomainServerCheckIn() {
auto accountManager = DependencyManager::get<AccountManager>(); auto accountManager = DependencyManager::get<AccountManager>();
const QUuid& connectionToken = _domainHandler.getConnectionToken(); const QUuid& connectionToken = _domainHandler.getConnectionToken();
bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull(); bool requiresUsernameSignature = !domainIsConnected && !connectionToken.isNull();
if (requiresUsernameSignature && !accountManager->getAccountInfo().hasPrivateKey()) { if (requiresUsernameSignature && !accountManager->getAccountInfo().hasPrivateKey()) {
qWarning() << "A keypair is required to present a username signature to the domain-server" 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()); QDataStream packetStream(domainPacket.get());
HifiSockAddr localSockAddr = _localSockAddr;
if (domainPacketType == PacketType::DomainConnectRequest) { if (domainPacketType == PacketType::DomainConnectRequest) {
#if (PR_BUILD || DEV_BUILD) #if (PR_BUILD || DEV_BUILD)
@ -361,13 +367,9 @@ void NodeList::sendDomainServerCheckIn() {
} }
#endif #endif
QUuid connectUUID; QUuid connectUUID = _domainHandler.getAssignmentUUID();
if (!_domainHandler.getAssignmentUUID().isNull()) { if (connectUUID.isNull() && _domainHandler.requiresICE()) {
// 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()) {
// this is a connect request and we're an interface client // this is a connect request and we're an interface client
// that used ice to discover the DS // that used ice to discover the DS
// so send our ICE client UUID with the connect request // 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 // if possible, include the MAC address for the current interface in our connect request
QString hardwareAddress; QString hardwareAddress;
for (auto networkInterface : QNetworkInterface::allInterfaces()) { for (auto networkInterface : QNetworkInterface::allInterfaces()) {
for (auto interfaceAddress : networkInterface.addressEntries()) { 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 // this is the interface whose local IP matches what we've detected the current IP to be
hardwareAddress = networkInterface.hardwareAddress(); hardwareAddress = networkInterface.hardwareAddress();
@ -410,10 +411,10 @@ void NodeList::sendDomainServerCheckIn() {
// pack our data to send to the domain-server including // 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) // 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(); packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
if (!_domainHandler.isConnected()) { if (!domainIsConnected) {
DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
packetStream << accountInfo.getUsername(); packetStream << accountInfo.getUsername();
@ -433,9 +434,9 @@ void NodeList::sendDomainServerCheckIn() {
checkinCount = std::min(checkinCount, MAX_CHECKINS_TOGETHER); checkinCount = std::min(checkinCount, MAX_CHECKINS_TOGETHER);
for (int i = 1; i < checkinCount; ++i) { for (int i = 1; i < checkinCount; ++i) {
auto packetCopy = domainPacket->createCopy(*domainPacket); 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)); QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
qCDebug(networking) << "Received packet from domain-server to remove node with UUID" << uuidStringWithoutCurlyBraces(nodeUUID); qCDebug(networking) << "Received packet from domain-server to remove node with UUID" << uuidStringWithoutCurlyBraces(nodeUUID);
killNodeWithUUID(nodeUUID); killNodeWithUUID(nodeUUID);
removeDelayedAdd(nodeUUID);
} }
void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
// setup variables to read into from QDataStream NewNodeInfo info;
qint8 nodeType;
QUuid nodeUUID, connectionSecretUUID;
HifiSockAddr nodePublicSocket, nodeLocalSocket;
NodePermissions permissions;
bool isReplicated;
Node::LocalID sessionLocalID;
packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions packetStream >> info.type
>> isReplicated >> sessionLocalID; >> 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 // if the public socket address is 0 then it's reachable at the same IP
// as the domain server // as the domain server
if (nodePublicSocket.getAddress().isNull()) { if (info.publicSocket.getAddress().isNull()) {
nodePublicSocket.setAddress(_domainHandler.getIP()); info.publicSocket.setAddress(_domainHandler.getIP());
} }
packetStream >> connectionSecretUUID; addNewNode(info);
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();
}
} }
void NodeList::sendAssignment(Assignment& assignment) { void NodeList::sendAssignment(Assignment& assignment) {
@ -785,7 +777,6 @@ void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) {
} }
void NodeList::startNodeHolePunch(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 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 // 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 // ping this node immediately
pingPunchForInactiveNode(node); 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() { void NodeList::handleNodePingTimeout() {

View file

@ -102,6 +102,11 @@ void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObjec
statsObject["io_stats"] = ioStats; statsObject["io_stats"] = ioStats;
QJsonObject assignmentStats;
assignmentStats["numQueuedCheckIns"] = _numQueuedCheckIns;
statsObject["assignmentStats"] = assignmentStats;
nodeList->sendStatsToDomainServer(statsObject); nodeList->sendStatsToDomainServer(statsObject);
} }
@ -119,10 +124,16 @@ void ThreadedAssignment::checkInWithDomainServerOrExit() {
stop(); stop();
} else { } else {
auto nodeList = DependencyManager::get<NodeList>(); 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 // increase the number of queued check ins
_numQueuedCheckIns++; _numQueuedCheckIns++;
if (_numQueuedCheckIns > 1) {
qCDebug(networking) << "Number of queued checkins = " << _numQueuedCheckIns;
}
} }
} }

View file

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

View file

@ -134,6 +134,9 @@
"bloom.bloomSize": { "bloom.bloomSize": {
"tooltip": "The radius of bloom. The higher the value, the larger the bloom." "tooltip": "The radius of bloom. The higher the value, the larger the bloom."
}, },
"avatarPriority": {
"tooltip": "Alter Avatars' update priorities."
},
"modelURL": { "modelURL": {
"tooltip": "A mesh model from an FBX or OBJ file." "tooltip": "A mesh model from an FBX or OBJ file."
}, },

View file

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

View file

@ -428,6 +428,13 @@ const GROUPS = [
propertyID: "bloom.bloomSize", propertyID: "bloom.bloomSize",
showPropertyRule: { "bloomMode": "enabled" }, 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() { Script.scriptEnding.connect(this, function() {
this.window.close(); 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) { setVisible: function(visible) {

View file

@ -47,24 +47,30 @@ These steps assume the hifi repository has been cloned to `~/hifi`.
### Windows ### Windows
1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/) 1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/)
1. Click the "add python to path" checkbox on the python installer 1. Click the "add python to path" checkbox on the python installer
1. After installation - add the path to python.exe to the Windows PATH environment variable. 1. After installation:
1. Open a new terminal
1. Enter `python` and hit enter
1. Verify that python is available (the prompt will change to `>>>`)
1. Type `exit()` and hit enter to close python
1. Install requests (a python library to download files from URLs)
`pip3 install requests`
1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/ 1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/
1. Open a new command prompt and run 1. Open a new command prompt and run
`aws configure` `aws configure`
1. Enter the AWS account number 1. Enter the AWS account number
1. Enter the secret key 1. Enter the secret key
1. Leave region name and ouput format as default [None] 1. Leave region name and ouput format as default [None]
1. Install the latest release of Boto3 via pip: 1. Install the latest release of Boto3 via pip (from a terminal):
`pip install boto3` `pip install boto3`
1. (First time) Download adb (Android Debug Bridge) from *https://dl.google.com/android/repository/platform-tools-latest-windows.zip* 1. (First time) Download adb (Android Debug Bridge) from *https://dl.google.com/android/repository/platform-tools-latest-windows.zip*
1. Copy the downloaded file to (for example) **C:\adb** and extract in place. 1. Copy the downloaded file to (for example) **C:\adb** and extract in place.
Verify you see *adb.exe* in **C:\adb\platform-tools\\**. Verify you see *adb.exe* in **C:\adb\platform-tools\\**.
1. After installation - add the path to adb.exe to the Windows PATH environment variable (note that it is in *adb\platform-tools*). 1. After installation - add the path to adb.exe to the Windows PATH environment variable (note that it is in *adb\platform-tools*).
1. `nitpick` is included in the High Fidelity installer but can also be downloaded from: 1. `nitpick` is included in the High Fidelity installer but can also be downloaded from (change X.X.X to correct version):
[here](<https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vX.X.X.dmg>).* [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vX.X.X.dmg>).*
### Mac ### Mac
1. (first time) Install brew 1. (First time) Install brew
In a terminal: In a terminal:
`/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
Note that you will need to press RETURN again, and will then be asked for your password. Note that you will need to press RETURN again, and will then be asked for your password.
@ -76,11 +82,13 @@ These steps assume the hifi repository has been cloned to `~/hifi`.
`open "/Applications/Python 3.7/Install Certificates.command"`. `open "/Applications/Python 3.7/Install Certificates.command"`.
This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates. This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates.
1. Verify that `/usr/local/bin/python3` exists. 1. Verify that `/usr/local/bin/python3` exists.
1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority:
In a terminal: In a terminal:
`curl -O https://bootstrap.pypa.io/get-pip.py` `curl -O https://bootstrap.pypa.io/get-pip.py`
In a terminal: In a terminal:
`python3 get-pip.py --user` `python3 get-pip.py --user`
1. Install requests (a python library to download files from URLs)
`pip3 install requests`
1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority:
1. Use pip to install the AWS CLI. 1. Use pip to install the AWS CLI.
`pip3 install awscli --upgrade --user` `pip3 install awscli --upgrade --user`
This will install aws in your user. For user XXX, aws will be located in ~/Library/Python/3.7/bin This will install aws in your user. For user XXX, aws will be located in ~/Library/Python/3.7/bin
@ -92,6 +100,16 @@ This is needed because the Mac Python supplied no longer links with the deprecat
1. Install the latest release of Boto3 via pip: pip3 install boto3 1. Install the latest release of Boto3 via pip: pip3 install boto3
1. (First time)Install adb (the Android Debug Bridge) - in a terminal: 1. (First time)Install adb (the Android Debug Bridge) - in a terminal:
`brew cask install android-platform-tools` `brew cask install android-platform-tools`
1. (First time) Set terminal privileges
1. Click on Apple icon (top left)
1. Select System Preferences...
1. Select Security & Privacy
1. Select Accessibility
1. Click on "Click the lock to make changes" and enter passsword if requested
1. Set Checkbox near *Terminal* to checked.
1. Click on "Click the lock to prevent furthur changes"
1. Close window
1. `nitpick` is included in the High Fidelity installer but can also be downloaded from: 1. `nitpick` is included in the High Fidelity installer but can also be downloaded from:
[here](<https://hifi-qa.s3.amazonaws.com/nitpick/Mac/nitpick-installer-vX.X.X.dmg>).* [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Mac/nitpick-installer-vX.X.X.dmg>).*
# Usage # Usage

View file

@ -27,7 +27,10 @@ AWSInterface::AWSInterface(QObject* parent) : QObject(parent) {
void AWSInterface::createWebPageFromResults(const QString& testResults, void AWSInterface::createWebPageFromResults(const QString& testResults,
const QString& workingDirectory, const QString& workingDirectory,
QCheckBox* updateAWSCheckBox, QCheckBox* updateAWSCheckBox,
QLineEdit* urlLineEdit) { QRadioButton* diffImageRadioButton,
QRadioButton* ssimImageRadionButton,
QLineEdit* urlLineEdit
) {
_workingDirectory = workingDirectory; _workingDirectory = workingDirectory;
// Verify filename is in correct format // Verify filename is in correct format
@ -52,6 +55,13 @@ void AWSInterface::createWebPageFromResults(const QString& testResults,
QString zipFilenameWithoutExtension = zipFilename.split('.')[0]; QString zipFilenameWithoutExtension = zipFilename.split('.')[0];
extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension); extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension);
if (diffImageRadioButton->isChecked()) {
_comparisonImageFilename = "Difference Image.png";
} else {
_comparisonImageFilename = "SSIM Image.png";
}
createHTMLFile(); createHTMLFile();
if (updateAWSCheckBox->isChecked()) { 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>Test</h1></th>\n";
stream << "\t\t\t\t<th><h1>Actual Image</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>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"; stream << "\t\t\t</tr>\n";
} }
} }
@ -378,12 +388,13 @@ void AWSInterface::createEntry(const int index, const QString& testResult, QText
QString folder; QString folder;
bool differenceFileFound; bool differenceFileFound;
if (isFailure) { if (isFailure) {
folder = FAILURES_FOLDER; folder = FAILURES_FOLDER;
differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/Difference Image.png"); differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/" + _comparisonImageFilename);
} else { } else {
folder = SUCCESSES_FOLDER; folder = SUCCESSES_FOLDER;
differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/Difference Image.png"); differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/" + _comparisonImageFilename);
} }
if (textResultsFileFound) { 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"; stream << "\t\t\t\t<td><img src=\"./" << folder << "/" << resultName << "/Expected Image.png\" width = \"576\" height = \"324\" ></td>\n";
if (differenceFileFound) { 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 { } else {
stream << "\t\t\t\t<td><h2>No Image Found</h2>\n"; stream << "\t\t\t\t<td><h2>No Image Found</h2>\n";
} }
@ -469,7 +480,7 @@ void AWSInterface::updateAWS() {
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not create 'addTestCases.py'"); "Could not create 'updateAWS.py'");
exit(-1); exit(-1);
} }
@ -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"; 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 << "/" stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png" << _comparisonImageFilename
<< "', 'rb')\n"; << "', '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"; 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 << "/" stream << "data = open('" << _workingDirectory << "/" << filename << "/"
<< "Difference Image.png" << _comparisonImageFilename
<< "', 'rb')\n"; << "', '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";
} }
} }
} }
@ -578,6 +589,7 @@ void AWSInterface::updateAWS() {
QProcess* process = new QProcess(); QProcess* process = new QProcess();
_busyWindow.setWindowTitle("Updating AWS");
connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); });
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,

View file

@ -14,6 +14,7 @@
#include <QCheckBox> #include <QCheckBox>
#include <QLineEdit> #include <QLineEdit>
#include <QObject> #include <QObject>
#include <QRadioButton>
#include <QTextStream> #include <QTextStream>
#include "BusyWindow.h" #include "BusyWindow.h"
@ -28,6 +29,8 @@ public:
void createWebPageFromResults(const QString& testResults, void createWebPageFromResults(const QString& testResults,
const QString& workingDirectory, const QString& workingDirectory,
QCheckBox* updateAWSCheckBox, QCheckBox* updateAWSCheckBox,
QRadioButton* diffImageRadioButton,
QRadioButton* ssimImageRadionButton,
QLineEdit* urlLineEdit); QLineEdit* urlLineEdit);
void extractTestFailuresFromZippedFolder(const QString& folderName); void extractTestFailuresFromZippedFolder(const QString& folderName);
@ -67,6 +70,9 @@ private:
QString AWS_BUCKET{ "hifi-qa" }; QString AWS_BUCKET{ "hifi-qa" };
QLineEdit* _urlLineEdit; QLineEdit* _urlLineEdit;
QString _comparisonImageFilename;
}; };
#endif // hifi_AWSInterface_h #endif // hifi_AWSInterface_h

View file

@ -16,12 +16,13 @@
QString AdbInterface::getAdbCommand() { QString AdbInterface::getAdbCommand() {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
if (_adbCommand.isNull()) { if (_adbCommand.isNull()) {
QString adbPath = PathUtils::getPathToExecutable("adb.exe"); QString adbExe{ "adb.exe" };
QString adbPath = PathUtils::getPathToExecutable(adbExe);
if (!adbPath.isNull()) { if (!adbPath.isNull()) {
_adbCommand = adbPath + _adbExe; _adbCommand = adbExe;
} else { } else {
QMessageBox::critical(0, "python.exe not found", QMessageBox::critical(0, "adb.exe not found",
"Please verify that pyton.exe is in the PATH"); "Please verify that adb.exe is in the PATH");
exit(-1); exit(-1);
} }
} }

View file

@ -17,12 +17,6 @@ public:
QString getAdbCommand(); QString getAdbCommand();
private: private:
#ifdef Q_OS_WIN
const QString _adbExe{ "adb.exe" };
#else
// Both Mac and Linux use "python"
const QString _adbExe{ "adb" };
#endif
QString _adbCommand; QString _adbCommand;
}; };

View file

@ -8,32 +8,66 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// //
#include "Downloader.h" #include "Downloader.h"
#include "PythonInterface.h"
#include <QtWidgets/QMessageBox> #include <QFile>
#include <QMessageBox>
#include <QProcess>
#include <QThread>
#include <QTextStream>
Downloader::Downloader(QUrl fileURL, QObject *parent) : QObject(parent) { Downloader::Downloader() {
_networkAccessManager.get(QNetworkRequest(fileURL)); PythonInterface* pythonInterface = new PythonInterface();
_pythonCommand = pythonInterface->getPythonCommand();
connect(
&_networkAccessManager, SIGNAL (finished(QNetworkReply*)),
this, SLOT (fileDownloaded(QNetworkReply*))
);
} }
void Downloader::fileDownloaded(QNetworkReply* reply) { void Downloader::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) {
QNetworkReply::NetworkError error = reply->error(); if (URLs.size() <= 0) {
if (error != QNetworkReply::NetworkError::NoError) {
QMessageBox::information(0, "Test Aborted", "Failed to download file: " + reply->errorString());
return; return;
} }
_downloadedData = reply->readAll(); QString filename = directoryName + "/downloadFiles.py";
if (QFile::exists(filename)) {
QFile::remove(filename);
}
QFile file(filename);
//emit a signal if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
reply->deleteLater(); QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
emit downloaded(); "Could not create 'downloadFiles.py'");
} exit(-1);
}
QByteArray Downloader::downloadedData() const { QTextStream stream(&file);
return _downloadedData;
stream << "import requests\n";
for (int i = 0; i < URLs.size(); ++i) {
stream << "\nurl = '" + URLs[i] + "'\n";
stream << "r = requests.get(url)\n";
stream << "open('" + directoryName + '/' + filenames [i] + "', 'wb').write(r.content)\n";
}
file.close();
#ifdef Q_OS_WIN
QProcess* process = new QProcess();
_busyWindow.setWindowTitle("Downloading Files");
connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); });
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
[=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); });
QStringList parameters = QStringList() << filename;
process->start(_pythonCommand, parameters);
#elif defined Q_OS_MAC
QProcess* process = new QProcess();
QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename;
process->start("sh", parameters);
// Wait for the last file to download
while (!QFile::exists(directoryName + '/' + filenames[filenames.length() - 1])) {
QThread::msleep(200);
}
#endif
} }

View file

@ -11,38 +11,19 @@
#ifndef hifi_downloader_h #ifndef hifi_downloader_h
#define hifi_downloader_h #define hifi_downloader_h
#include <QObject> #include "BusyWindow.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QDebug>
#include <QObject> #include <QObject>
#include <QByteArray>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
class Downloader : public QObject { class Downloader : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit Downloader(QUrl fileURL, QObject *parent = 0); Downloader();
QByteArray downloadedData() const; void downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller);
signals:
void downloaded();
private slots:
void fileDownloaded(QNetworkReply* pReply);
private: private:
QNetworkAccessManager _networkAccessManager; QString _pythonCommand;
QByteArray _downloadedData; BusyWindow _busyWindow;
}; };
#endif // hifi_downloader_h #endif // hifi_downloader_h

View file

@ -14,7 +14,7 @@
// Computes SSIM - see https://en.wikipedia.org/wiki/Structural_similarity // Computes SSIM - see https://en.wikipedia.org/wiki/Structural_similarity
// The value is computed for the luminance component and the average value is returned // 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 int L = 255; // (2^number of bits per pixel) - 1
const double K1 { 0.01 }; const double K1 { 0.01 };
@ -39,8 +39,13 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co
double p[WIN_SIZE * WIN_SIZE]; double p[WIN_SIZE * WIN_SIZE];
double q[WIN_SIZE * WIN_SIZE]; double q[WIN_SIZE * WIN_SIZE];
_ssimResults.results.clear();
int windowCounter{ 0 }; int windowCounter{ 0 };
double ssim{ 0.0 }; double ssim{ 0.0 };
double min { 1.0 };
double max { -1.0 };
while (x < expectedImage.width()) { while (x < expectedImage.width()) {
int lastX = x + WIN_SIZE - 1; int lastX = x + WIN_SIZE - 1;
if (lastX > expectedImage.width() - 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 numerator = (2.0 * mP * mQ + c1) * (2.0 * sigPQ + c2);
double denominator = (mP * mP + mQ * mQ + c1) * (sigsqP + sigsqQ + 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; ++windowCounter;
y += WIN_SIZE; y += WIN_SIZE;
@ -106,5 +117,17 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co
y = 0; 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 #ifndef hifi_ImageComparer_h
#define hifi_ImageComparer_h #define hifi_ImageComparer_h
#include "common.h"
#include <QtCore/QString> #include <QtCore/QString>
#include <QImage> #include <QImage>
class ImageComparer { class ImageComparer {
public: 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 #endif // hifi_ImageComparer_h

View file

@ -21,7 +21,7 @@ MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) {
diffImage->setScaledContents(true); 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 // Create an empty difference image if the images differ in size
if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) { if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) {
return QPixmap(); return QPixmap();
@ -60,7 +60,7 @@ QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultIma
return resultPixmap; return resultPixmap;
} }
void MismatchWindow::setTestResult(TestResult testResult) { void MismatchWindow::setTestResult(const TestResult& testResult) {
errorLabel->setText("Similarity: " + QString::number(testResult._error)); errorLabel->setText("Similarity: " + QString::number(testResult._error));
imagePath->setText("Path to test: " + testResult._pathname); imagePath->setText("Path to test: " + testResult._pathname);
@ -99,3 +99,36 @@ void MismatchWindow::on_abortTestsButton_clicked() {
QPixmap MismatchWindow::getComparisonImage() { QPixmap MismatchWindow::getComparisonImage() {
return _diffPixmap; 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: public:
MismatchWindow(QWidget *parent = Q_NULLPTR); MismatchWindow(QWidget *parent = Q_NULLPTR);
void setTestResult(TestResult testResult); void setTestResult(const TestResult& testResult);
UserResponse getUserResponse() { return _userResponse; } UserResponse getUserResponse() { return _userResponse; }
QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage); QPixmap computeDiffPixmap(const QImage& expectedImage, const QImage& resultImage);
QPixmap getComparisonImage(); QPixmap getComparisonImage();
QPixmap getSSIMResultsImage(const SSIMResults& ssimResults);
private slots: private slots:
void on_passTestButton_clicked(); void on_passTestButton_clicked();

View file

@ -24,8 +24,6 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.progressBar->setVisible(false); _ui.progressBar->setVisible(false);
_ui.tabWidget->setCurrentIndex(0); _ui.tabWidget->setCurrentIndex(0);
_signalMapper = new QSignalMapper();
connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closePushbutton_clicked); connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closePushbutton_clicked);
connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about); connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about);
connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content); connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content);
@ -48,10 +46,8 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
} }
Nitpick::~Nitpick() { Nitpick::~Nitpick() {
delete _signalMapper; if (_testCreator) {
delete _testCreator;
if (_test) {
delete _test;
} }
if (_testRunnerDesktop) { if (_testRunnerDesktop) {
@ -64,10 +60,10 @@ Nitpick::~Nitpick() {
} }
void Nitpick::setup() { void Nitpick::setup() {
if (_test) { if (_testCreator) {
delete _test; delete _testCreator;
} }
_test = new Test(_ui.progressBar, _ui.checkBoxInteractiveMode); _testCreator = new TestCreator(_ui.progressBar, _ui.checkBoxInteractiveMode);
std::vector<QCheckBox*> dayCheckboxes; std::vector<QCheckBox*> dayCheckboxes;
dayCheckboxes.emplace_back(_ui.mondayCheckBox); dayCheckboxes.emplace_back(_ui.mondayCheckBox);
@ -99,9 +95,12 @@ void Nitpick::setup() {
timeEditCheckboxes, timeEditCheckboxes,
timeEdits, timeEdits,
_ui.workingFolderRunOnDesktopLabel, _ui.workingFolderRunOnDesktopLabel,
_ui.checkBoxServerless, _ui.checkBoxServerless,
_ui.usePreviousInstallationOnDesktopCheckBox,
_ui.runLatestOnDesktopCheckBox, _ui.runLatestOnDesktopCheckBox,
_ui.urlOnDesktopLineEdit, _ui.urlOnDesktopLineEdit,
_ui.runFullSuiteOnDesktopCheckBox,
_ui.scriptURLOnDesktopLineEdit,
_ui.runNowPushbutton, _ui.runNowPushbutton,
_ui.statusLabelOnDesktop _ui.statusLabelOnDesktop
); );
@ -118,8 +117,11 @@ void Nitpick::setup() {
_ui.downloadAPKPushbutton, _ui.downloadAPKPushbutton,
_ui.installAPKPushbutton, _ui.installAPKPushbutton,
_ui.runInterfacePushbutton, _ui.runInterfacePushbutton,
_ui.usePreviousInstallationOnMobileCheckBox,
_ui.runLatestOnMobileCheckBox, _ui.runLatestOnMobileCheckBox,
_ui.urlOnMobileLineEdit, _ui.urlOnMobileLineEdit,
_ui.runFullSuiteOnMobileCheckBox,
_ui.scriptURLOnMobileLineEdit,
_ui.statusLabelOnMobile _ui.statusLabelOnMobile
); );
} }
@ -130,7 +132,7 @@ void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine,
const QString& branch, const QString& branch,
const QString& user const QString& user
) { ) {
_test->startTestsEvaluation(isRunningFromCommandLine, isRunningInAutomaticTestRun, snapshotDirectory, branch, user); _testCreator->startTestsEvaluation(isRunningFromCommandLine, isRunningInAutomaticTestRun, snapshotDirectory, branch, user);
} }
void Nitpick::on_tabWidget_currentChanged(int index) { void Nitpick::on_tabWidget_currentChanged(int index) {
@ -148,48 +150,44 @@ void Nitpick::on_tabWidget_currentChanged(int index) {
} }
} }
void Nitpick::on_evaluateTestsPushbutton_clicked() {
_test->startTestsEvaluation(false, false);
}
void Nitpick::on_createRecursiveScriptPushbutton_clicked() { void Nitpick::on_createRecursiveScriptPushbutton_clicked() {
_test->createRecursiveScript(); _testCreator->createRecursiveScript();
} }
void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() { void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() {
_test->createAllRecursiveScripts(); _testCreator->createAllRecursiveScripts();
} }
void Nitpick::on_createTestsPushbutton_clicked() { void Nitpick::on_createTestsPushbutton_clicked() {
_test->createTests(_ui.clientProfileComboBox->currentText()); _testCreator->createTests(_ui.clientProfileComboBox->currentText());
} }
void Nitpick::on_createMDFilePushbutton_clicked() { void Nitpick::on_createMDFilePushbutton_clicked() {
_test->createMDFile(); _testCreator->createMDFile();
} }
void Nitpick::on_createAllMDFilesPushbutton_clicked() { void Nitpick::on_createAllMDFilesPushbutton_clicked() {
_test->createAllMDFiles(); _testCreator->createAllMDFiles();
} }
void Nitpick::on_createTestAutoScriptPushbutton_clicked() { void Nitpick::on_createTestAutoScriptPushbutton_clicked() {
_test->createTestAutoScript(); _testCreator->createTestAutoScript();
} }
void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() { void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() {
_test->createAllTestAutoScripts(); _testCreator->createAllTestAutoScripts();
} }
void Nitpick::on_createTestsOutlinePushbutton_clicked() { void Nitpick::on_createTestsOutlinePushbutton_clicked() {
_test->createTestsOutline(); _testCreator->createTestsOutline();
} }
void Nitpick::on_createTestRailTestCasesPushbutton_clicked() { void Nitpick::on_createTestRailTestCasesPushbutton_clicked() {
_test->createTestRailTestCases(); _testCreator->createTestRailTestCases();
} }
void Nitpick::on_createTestRailRunButton_clicked() { void Nitpick::on_createTestRailRunButton_clicked() {
_test->createTestRailRun(); _testCreator->createTestRailRun();
} }
void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() { void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() {
@ -206,16 +204,25 @@ void Nitpick::on_runNowPushbutton_clicked() {
_testRunnerDesktop->run(); _testRunnerDesktop->run();
} }
void Nitpick::on_usePreviousInstallationOnDesktopCheckBox_clicked() {
_ui.runLatestOnDesktopCheckBox->setEnabled(!_ui.usePreviousInstallationOnDesktopCheckBox->isChecked());
_ui.urlOnDesktopLineEdit->setEnabled(!_ui.usePreviousInstallationOnDesktopCheckBox->isChecked() && !_ui.runLatestOnDesktopCheckBox->isChecked());
}
void Nitpick::on_runLatestOnDesktopCheckBox_clicked() { void Nitpick::on_runLatestOnDesktopCheckBox_clicked() {
_ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked()); _ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked());
} }
void Nitpick::on_runFullSuiteOnDesktopCheckBox_clicked() {
_ui.scriptURLOnDesktopLineEdit->setEnabled(!_ui.runFullSuiteOnDesktopCheckBox->isChecked());
}
void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) {
_testRunnerDesktop->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); _testRunnerDesktop->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures);
} }
void Nitpick::on_updateTestRailRunResultsPushbutton_clicked() { void Nitpick::on_updateTestRailRunResultsPushbutton_clicked() {
_test->updateTestRailRunResult(); _testCreator->updateTestRailRunResult();
} }
// To toggle between show and hide // To toggle between show and hide
@ -242,85 +249,24 @@ void Nitpick::on_showTaskbarPushbutton_clicked() {
#endif #endif
} }
void Nitpick::on_evaluateTestsPushbutton_clicked() {
_testCreator->startTestsEvaluation(false, false);
}
void Nitpick::on_closePushbutton_clicked() { void Nitpick::on_closePushbutton_clicked() {
exit(0); exit(0);
} }
void Nitpick::on_createPythonScriptRadioButton_clicked() { void Nitpick::on_createPythonScriptRadioButton_clicked() {
_test->setTestRailCreateMode(PYTHON); _testCreator->setTestRailCreateMode(PYTHON);
} }
void Nitpick::on_createXMLScriptRadioButton_clicked() { void Nitpick::on_createXMLScriptRadioButton_clicked() {
_test->setTestRailCreateMode(XML); _testCreator->setTestRailCreateMode(XML);
} }
void Nitpick::on_createWebPagePushbutton_clicked() { void Nitpick::on_createWebPagePushbutton_clicked() {
_test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit);
}
void Nitpick::downloadFile(const QUrl& url) {
_downloaders.emplace_back(new Downloader(url, this));
connect(_downloaders[_index], SIGNAL(downloaded()), _signalMapper, SLOT(map()));
_signalMapper->setMapping(_downloaders[_index], _index);
++_index;
}
void Nitpick::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) {
connect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int)));
_directoryName = directoryName;
_filenames = filenames;
_caller = caller;
_numberOfFilesToDownload = URLs.size();
_numberOfFilesDownloaded = 0;
_index = 0;
_ui.progressBar->setMinimum(0);
_ui.progressBar->setMaximum(_numberOfFilesToDownload - 1);
_ui.progressBar->setValue(0);
_ui.progressBar->setVisible(true);
foreach (auto downloader, _downloaders) {
delete downloader;
}
_downloaders.clear();
for (int i = 0; i < _numberOfFilesToDownload; ++i) {
downloadFile(URLs[i]);
}
}
void Nitpick::saveFile(int index) {
try {
QFile file(_directoryName + "/" + _filenames[index]);
file.open(QIODevice::WriteOnly);
file.write(_downloaders[index]->downloadedData());
file.close();
} catch (...) {
QMessageBox::information(0, "Test Aborted", "Failed to save file: " + _filenames[index]);
_ui.progressBar->setVisible(false);
return;
}
++_numberOfFilesDownloaded;
if (_numberOfFilesDownloaded == _numberOfFilesToDownload) {
disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int)));
if (_caller == _test) {
_test->finishTestsEvaluation();
} else if (_caller == _testRunnerDesktop) {
_testRunnerDesktop->downloadComplete();
} else if (_caller == _testRunnerMobile) {
_testRunnerMobile->downloadComplete();
}
_ui.progressBar->setVisible(false);
} else {
_ui.progressBar->setValue(_numberOfFilesDownloaded);
}
} }
void Nitpick::about() { void Nitpick::about() {
@ -360,10 +306,19 @@ void Nitpick::on_connectDevicePushbutton_clicked() {
_testRunnerMobile->connectDevice(); _testRunnerMobile->connectDevice();
} }
void Nitpick::on_usePreviousInstallationOnMobileCheckBox_clicked() {
_ui.runLatestOnMobileCheckBox->setEnabled(!_ui.usePreviousInstallationOnMobileCheckBox->isChecked());
_ui.urlOnMobileLineEdit->setEnabled(!_ui.usePreviousInstallationOnMobileCheckBox->isChecked() && !_ui.runLatestOnMobileCheckBox->isChecked());
}
void Nitpick::on_runLatestOnMobileCheckBox_clicked() { void Nitpick::on_runLatestOnMobileCheckBox_clicked() {
_ui.urlOnMobileLineEdit->setEnabled(!_ui.runLatestOnMobileCheckBox->isChecked()); _ui.urlOnMobileLineEdit->setEnabled(!_ui.runLatestOnMobileCheckBox->isChecked());
} }
void Nitpick::on_runFullSuiteOnMobileCheckBox_clicked() {
_ui.scriptURLOnMobileLineEdit->setEnabled(!_ui.runFullSuiteOnMobileCheckBox->isChecked());
}
void Nitpick::on_downloadAPKPushbutton_clicked() { void Nitpick::on_downloadAPKPushbutton_clicked() {
_testRunnerMobile->downloadAPK(); _testRunnerMobile->downloadAPK();
} }

View file

@ -11,12 +11,10 @@
#define hifi_Nitpick_h #define hifi_Nitpick_h
#include <QtWidgets/QMainWindow> #include <QtWidgets/QMainWindow>
#include <QSignalMapper>
#include <QTextEdit> #include <QTextEdit>
#include "ui_Nitpick.h" #include "ui_Nitpick.h"
#include "Downloader.h" #include "TestCreator.h"
#include "Test.h"
#include "TestRunnerDesktop.h" #include "TestRunnerDesktop.h"
#include "TestRunnerMobile.h" #include "TestRunnerMobile.h"
@ -38,9 +36,6 @@ public:
void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures); void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures);
void downloadFile(const QUrl& url);
void downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void* caller);
void setUserText(const QString& user); void setUserText(const QString& user);
QString getSelectedUser(); QString getSelectedUser();
@ -56,7 +51,6 @@ private slots:
void on_tabWidget_currentChanged(int index); void on_tabWidget_currentChanged(int index);
void on_evaluateTestsPushbutton_clicked();
void on_createRecursiveScriptPushbutton_clicked(); void on_createRecursiveScriptPushbutton_clicked();
void on_createAllRecursiveScriptsPushbutton_clicked(); void on_createAllRecursiveScriptsPushbutton_clicked();
void on_createTestsPushbutton_clicked(); void on_createTestsPushbutton_clicked();
@ -75,27 +69,32 @@ private slots:
void on_setWorkingFolderRunOnDesktopPushbutton_clicked(); void on_setWorkingFolderRunOnDesktopPushbutton_clicked();
void on_runNowPushbutton_clicked(); void on_runNowPushbutton_clicked();
void on_usePreviousInstallationOnDesktopCheckBox_clicked();
void on_runLatestOnDesktopCheckBox_clicked(); void on_runLatestOnDesktopCheckBox_clicked();
void on_runFullSuiteOnDesktopCheckBox_clicked();
void on_updateTestRailRunResultsPushbutton_clicked(); void on_updateTestRailRunResultsPushbutton_clicked();
void on_hideTaskbarPushbutton_clicked(); void on_hideTaskbarPushbutton_clicked();
void on_showTaskbarPushbutton_clicked(); void on_showTaskbarPushbutton_clicked();
void on_evaluateTestsPushbutton_clicked();
void on_createPythonScriptRadioButton_clicked(); void on_createPythonScriptRadioButton_clicked();
void on_createXMLScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked();
void on_createWebPagePushbutton_clicked(); void on_createWebPagePushbutton_clicked();
void saveFile(int index);
void about(); void about();
void content(); void content();
// Run on Mobile controls // Run on Mobile controls
void on_setWorkingFolderRunOnMobilePushbutton_clicked(); void on_setWorkingFolderRunOnMobilePushbutton_clicked();
void on_connectDevicePushbutton_clicked(); void on_connectDevicePushbutton_clicked();
void on_usePreviousInstallationOnMobileCheckBox_clicked();
void on_runLatestOnMobileCheckBox_clicked(); void on_runLatestOnMobileCheckBox_clicked();
void on_runFullSuiteOnMobileCheckBox_clicked();
void on_downloadAPKPushbutton_clicked(); void on_downloadAPKPushbutton_clicked();
void on_installAPKPushbutton_clicked(); void on_installAPKPushbutton_clicked();
@ -105,28 +104,13 @@ private slots:
private: private:
Ui::NitpickClass _ui; Ui::NitpickClass _ui;
Test* _test{ nullptr }; TestCreator* _testCreator{ nullptr };
TestRunnerDesktop* _testRunnerDesktop{ nullptr }; TestRunnerDesktop* _testRunnerDesktop{ nullptr };
TestRunnerMobile* _testRunnerMobile{ nullptr }; TestRunnerMobile* _testRunnerMobile{ nullptr };
std::vector<Downloader*> _downloaders;
// local storage for parameters - folder to store downloaded files in, and a list of their names
QString _directoryName;
QStringList _filenames;
// Used to enable passing a parameter to slots
QSignalMapper* _signalMapper;
int _numberOfFilesToDownload{ 0 };
int _numberOfFilesDownloaded{ 0 };
int _index{ 0 };
bool _isRunningFromCommandline{ false }; bool _isRunningFromCommandline{ false };
void* _caller;
QStringList clientProfiles; QStringList clientProfiles;
}; };

View file

@ -1,5 +1,5 @@
// //
// Test.cpp // TestCreator.cpp
// //
// Created by Nissim Hadar on 2 Nov 2017. // Created by Nissim Hadar on 2 Nov 2017.
// Copyright 2013 High Fidelity, Inc. // Copyright 2013 High Fidelity, Inc.
@ -7,7 +7,7 @@
// Distributed under the Apache License, Version 2.0. // Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// //
#include "Test.h" #include "TestCreator.h"
#include <assert.h> #include <assert.h>
#include <QtCore/QTextStream> #include <QtCore/QTextStream>
@ -24,7 +24,9 @@ extern Nitpick* nitpick;
#include <math.h> #include <math.h>
Test::Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) : _awsInterface(NULL) { TestCreator::TestCreator(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) : _awsInterface(NULL) {
_downloader = new Downloader();
_progressBar = progressBar; _progressBar = progressBar;
_checkBoxInteractiveMode = checkBoxInteractiveMode; _checkBoxInteractiveMode = checkBoxInteractiveMode;
@ -36,7 +38,7 @@ Test::Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) : _aws
} }
} }
bool Test::createTestResultsFolderPath(const QString& directory) { bool TestCreator::createTestResultsFolderPath(const QString& directory) {
QDateTime now = QDateTime::currentDateTime(); QDateTime now = QDateTime::currentDateTime();
_testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT) + "(local)[" + QHostInfo::localHostName() + "]"; _testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT) + "(local)[" + QHostInfo::localHostName() + "]";
QDir testResultsFolder(_testResultsFolderPath); QDir testResultsFolder(_testResultsFolderPath);
@ -45,7 +47,7 @@ bool Test::createTestResultsFolderPath(const QString& directory) {
return QDir().mkdir(_testResultsFolderPath); return QDir().mkdir(_testResultsFolderPath);
} }
QString Test::zipAndDeleteTestResultsFolder() { QString TestCreator::zipAndDeleteTestResultsFolder() {
QString zippedResultsFileName { _testResultsFolderPath + ".zip" }; QString zippedResultsFileName { _testResultsFolderPath + ".zip" };
QFileInfo fileInfo(zippedResultsFileName); QFileInfo fileInfo(zippedResultsFileName);
if (fileInfo.exists()) { if (fileInfo.exists()) {
@ -65,7 +67,7 @@ QString Test::zipAndDeleteTestResultsFolder() {
return zippedResultsFileName; return zippedResultsFileName;
} }
int Test::compareImageLists() { int TestCreator::compareImageLists() {
_progressBar->setMinimum(0); _progressBar->setMinimum(0);
_progressBar->setMaximum(_expectedImagesFullFilenames.length() - 1); _progressBar->setMaximum(_expectedImagesFullFilenames.length() - 1);
_progressBar->setValue(0); _progressBar->setValue(0);
@ -89,23 +91,25 @@ int Test::compareImageLists() {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size");
similarityIndex = -100.0; similarityIndex = -100.0;
} else { } else {
similarityIndex = _imageComparer.compareImages(resultImage, expectedImage); _imageComparer.compareImages(resultImage, expectedImage);
similarityIndex = _imageComparer.getSSIMValue();
} }
TestResult testResult = TestResult{ TestResult testResult = TestResult{
(float)similarityIndex, (float)similarityIndex,
_expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) _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(_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); _mismatchWindow.setTestResult(testResult);
if (similarityIndex < THRESHOLD) { if (similarityIndex < THRESHOLD) {
++numberOfFailures; ++numberOfFailures;
if (!isInteractiveMode) { if (!isInteractiveMode) {
appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true);
} else { } else {
_mismatchWindow.exec(); _mismatchWindow.exec();
@ -113,7 +117,7 @@ int Test::compareImageLists() {
case USER_RESPONSE_PASS: case USER_RESPONSE_PASS:
break; break;
case USE_RESPONSE_FAIL: case USE_RESPONSE_FAIL:
appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true);
break; break;
case USER_RESPONSE_ABORT: case USER_RESPONSE_ABORT:
keepOn = false; keepOn = false;
@ -124,7 +128,7 @@ int Test::compareImageLists() {
} }
} }
} else { } else {
appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), false); appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), false);
} }
_progressBar->setValue(i); _progressBar->setValue(i);
@ -134,7 +138,7 @@ int Test::compareImageLists() {
return numberOfFailures; return numberOfFailures;
} }
int Test::checkTextResults() { int TestCreator::checkTextResults() {
// Create lists of failed and passed tests // Create lists of failed and passed tests
QStringList nameFilterFailed; QStringList nameFilterFailed;
nameFilterFailed << "*.failed.txt"; nameFilterFailed << "*.failed.txt";
@ -144,7 +148,7 @@ int Test::checkTextResults() {
nameFilterPassed << "*.passed.txt"; nameFilterPassed << "*.passed.txt";
QStringList testsPassed = QDir(_snapshotDirectory).entryList(nameFilterPassed, QDir::Files, QDir::Name); QStringList testsPassed = QDir(_snapshotDirectory).entryList(nameFilterPassed, QDir::Files, QDir::Name);
// Add results to Test Results folder // Add results to TestCreator Results folder
foreach(QString currentFilename, testsFailed) { foreach(QString currentFilename, testsFailed) {
appendTestResultsToFile(currentFilename, true); appendTestResultsToFile(currentFilename, true);
} }
@ -156,7 +160,7 @@ int Test::checkTextResults() {
return testsFailed.length(); return testsFailed.length();
} }
void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed) { void TestCreator::appendTestResultsToFile(const TestResult& testResult, const QPixmap& comparisonImage, const QPixmap& ssimResultsImage, bool hasFailed) {
// Critical error if Test Results folder does not exist // Critical error if Test Results folder does not exist
if (!QDir().exists(_testResultsFolderPath)) { if (!QDir().exists(_testResultsFolderPath)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found"); QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found");
@ -191,7 +195,7 @@ void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImag
// Create text file describing the failure // Create text file describing the failure
QTextStream stream(&descriptionFile); QTextStream stream(&descriptionFile);
stream << "Test in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' stream << "TestCreator in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/'
stream << "Expected image was " << testResult._expectedImageFilename << endl; stream << "Expected image was " << testResult._expectedImageFilename << endl;
stream << "Actual image was " << testResult._actualImageFilename << endl; stream << "Actual image was " << testResult._actualImageFilename << endl;
stream << "Similarity index was " << testResult._error << endl; stream << "Similarity index was " << testResult._error << endl;
@ -217,9 +221,12 @@ void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImag
} }
comparisonImage.save(resultFolderPath + "/" + "Difference Image.png"); comparisonImage.save(resultFolderPath + "/" + "Difference Image.png");
// Save the SSIM results image
ssimResultsImage.save(resultFolderPath + "/" + "SSIM Image.png");
} }
void::Test::appendTestResultsToFile(QString testResultFilename, bool hasFailed) { void::TestCreator::appendTestResultsToFile(QString testResultFilename, bool hasFailed) {
// The test name includes everything until the penultimate period // The test name includes everything until the penultimate period
QString testNameTemp = testResultFilename.left(testResultFilename.lastIndexOf('.')); QString testNameTemp = testResultFilename.left(testResultFilename.lastIndexOf('.'));
QString testName = testResultFilename.left(testNameTemp.lastIndexOf('.')); QString testName = testResultFilename.left(testNameTemp.lastIndexOf('.'));
@ -246,7 +253,7 @@ void::Test::appendTestResultsToFile(QString testResultFilename, bool hasFailed)
} }
} }
void Test::startTestsEvaluation(const bool isRunningFromCommandLine, void TestCreator::startTestsEvaluation(const bool isRunningFromCommandLine,
const bool isRunningInAutomaticTestRun, const bool isRunningInAutomaticTestRun,
const QString& snapshotDirectory, const QString& snapshotDirectory,
const QString& branchFromCommandLine, const QString& branchFromCommandLine,
@ -319,10 +326,11 @@ void Test::startTestsEvaluation(const bool isRunningFromCommandLine,
} }
} }
nitpick->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this); _downloader->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this);
finishTestsEvaluation();
} }
void Test::finishTestsEvaluation() { void TestCreator::finishTestsEvaluation() {
// First - compare the pairs of images // First - compare the pairs of images
int numberOfFailures = compareImageLists(); int numberOfFailures = compareImageLists();
@ -348,7 +356,7 @@ void Test::finishTestsEvaluation() {
} }
} }
bool Test::isAValidDirectory(const QString& pathname) { bool TestCreator::isAValidDirectory(const QString& pathname) {
// Only process directories // Only process directories
QDir dir(pathname); QDir dir(pathname);
if (!dir.exists()) { if (!dir.exists()) {
@ -363,7 +371,7 @@ bool Test::isAValidDirectory(const QString& pathname) {
return true; return true;
} }
QString Test::extractPathFromTestsDown(const QString& fullPath) { QString TestCreator::extractPathFromTestsDown(const QString& fullPath) {
// `fullPath` includes the full path to the test. We need the portion below (and including) `tests` // `fullPath` includes the full path to the test. We need the portion below (and including) `tests`
QStringList pathParts = fullPath.split('/'); QStringList pathParts = fullPath.split('/');
int i{ 0 }; int i{ 0 };
@ -384,14 +392,14 @@ QString Test::extractPathFromTestsDown(const QString& fullPath) {
return partialPath; return partialPath;
} }
void Test::includeTest(QTextStream& textStream, const QString& testPathname) { void TestCreator::includeTest(QTextStream& textStream, const QString& testPathname) {
QString partialPath = extractPathFromTestsDown(testPathname); QString partialPath = extractPathFromTestsDown(testPathname);
QString partialPathWithoutTests = partialPath.right(partialPath.length() - 7); QString partialPathWithoutTests = partialPath.right(partialPath.length() - 7);
textStream << "Script.include(testsRootPath + \"" << partialPathWithoutTests + "\");" << endl; textStream << "Script.include(testsRootPath + \"" << partialPathWithoutTests + "\");" << endl;
} }
void Test::createTests(const QString& clientProfile) { void TestCreator::createTests(const QString& clientProfile) {
// Rename files sequentially, as ExpectedResult_00000.png, ExpectedResult_00001.png and so on // Rename files sequentially, as ExpectedResult_00000.png, ExpectedResult_00001.png and so on
// Any existing expected result images will be deleted // Any existing expected result images will be deleted
QString previousSelection = _snapshotDirectory; QString previousSelection = _snapshotDirectory;
@ -469,7 +477,7 @@ void Test::createTests(const QString& clientProfile) {
QMessageBox::information(0, "Success", "Test images have been created"); QMessageBox::information(0, "Success", "Test images have been created");
} }
ExtractedText Test::getTestScriptLines(QString testFileName) { ExtractedText TestCreator::getTestScriptLines(QString testFileName) {
ExtractedText relevantTextFromTest; ExtractedText relevantTextFromTest;
QFile inputFile(testFileName); QFile inputFile(testFileName);
@ -534,7 +542,7 @@ ExtractedText Test::getTestScriptLines(QString testFileName) {
return relevantTextFromTest; return relevantTextFromTest;
} }
bool Test::createFileSetup() { bool TestCreator::createFileSetup() {
// Folder selection // Folder selection
QString previousSelection = _testDirectory; QString previousSelection = _testDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
@ -554,7 +562,7 @@ bool Test::createFileSetup() {
return true; return true;
} }
bool Test::createAllFilesSetup() { bool TestCreator::createAllFilesSetup() {
// Select folder to start recursing from // Select folder to start recursing from
QString previousSelection = _testsRootDirectory; QString previousSelection = _testsRootDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
@ -576,7 +584,7 @@ bool Test::createAllFilesSetup() {
// Create an MD file for a user-selected test. // Create an MD file for a user-selected test.
// The folder selected must contain a script named "test.js", the file produced is named "test.md" // The folder selected must contain a script named "test.js", the file produced is named "test.md"
void Test::createMDFile() { void TestCreator::createMDFile() {
if (!createFileSetup()) { if (!createFileSetup()) {
return; return;
} }
@ -586,7 +594,7 @@ void Test::createMDFile() {
} }
} }
void Test::createAllMDFiles() { void TestCreator::createAllMDFiles() {
if (!createAllFilesSetup()) { if (!createAllFilesSetup()) {
return; return;
} }
@ -618,7 +626,7 @@ void Test::createAllMDFiles() {
QMessageBox::information(0, "Success", "MD files have been created"); QMessageBox::information(0, "Success", "MD files have been created");
} }
bool Test::createMDFile(const QString& directory) { bool TestCreator::createMDFile(const QString& directory) {
// Verify folder contains test.js file // Verify folder contains test.js file
QString testFileName(directory + "/" + TEST_FILENAME); QString testFileName(directory + "/" + TEST_FILENAME);
QFileInfo testFileInfo(testFileName); QFileInfo testFileInfo(testFileName);
@ -638,7 +646,7 @@ bool Test::createMDFile(const QString& directory) {
QTextStream stream(&mdFile); QTextStream stream(&mdFile);
//Test title //TestCreator title
QString testName = testScriptLines.title; QString testName = testScriptLines.title;
stream << "# " << testName << "\n"; stream << "# " << testName << "\n";
@ -670,7 +678,7 @@ bool Test::createMDFile(const QString& directory) {
return true; return true;
} }
void Test::createTestAutoScript() { void TestCreator::createTestAutoScript() {
if (!createFileSetup()) { if (!createFileSetup()) {
return; return;
} }
@ -680,7 +688,7 @@ void Test::createTestAutoScript() {
} }
} }
void Test::createAllTestAutoScripts() { void TestCreator::createAllTestAutoScripts() {
if (!createAllFilesSetup()) { if (!createAllFilesSetup()) {
return; return;
} }
@ -712,7 +720,7 @@ void Test::createAllTestAutoScripts() {
QMessageBox::information(0, "Success", "All 'testAuto.js' scripts have been created"); QMessageBox::information(0, "Success", "All 'testAuto.js' scripts have been created");
} }
bool Test::createTestAutoScript(const QString& directory) { bool TestCreator::createTestAutoScript(const QString& directory) {
// Verify folder contains test.js file // Verify folder contains test.js file
QString testFileName(directory + "/" + TEST_FILENAME); QString testFileName(directory + "/" + TEST_FILENAME);
QFileInfo testFileInfo(testFileName); QFileInfo testFileInfo(testFileName);
@ -743,7 +751,7 @@ bool Test::createTestAutoScript(const QString& directory) {
// Creates a single script in a user-selected folder. // Creates a single script in a user-selected folder.
// This script will run all text.js scripts in every applicable sub-folder // This script will run all text.js scripts in every applicable sub-folder
void Test::createRecursiveScript() { void TestCreator::createRecursiveScript() {
if (!createFileSetup()) { if (!createFileSetup()) {
return; return;
} }
@ -753,7 +761,7 @@ void Test::createRecursiveScript() {
} }
// This method creates a `testRecursive.js` script in every sub-folder. // This method creates a `testRecursive.js` script in every sub-folder.
void Test::createAllRecursiveScripts() { void TestCreator::createAllRecursiveScripts() {
if (!createAllFilesSetup()) { if (!createAllFilesSetup()) {
return; return;
} }
@ -763,7 +771,7 @@ void Test::createAllRecursiveScripts() {
QMessageBox::information(0, "Success", "Scripts have been created"); QMessageBox::information(0, "Success", "Scripts have been created");
} }
void Test::createAllRecursiveScripts(const QString& directory) { void TestCreator::createAllRecursiveScripts(const QString& directory) {
QDirIterator it(directory, QDirIterator::Subdirectories); QDirIterator it(directory, QDirIterator::Subdirectories);
while (it.hasNext()) { while (it.hasNext()) {
@ -775,7 +783,7 @@ void Test::createAllRecursiveScripts(const QString& directory) {
} }
} }
void Test::createRecursiveScript(const QString& directory, bool interactiveMode) { void TestCreator::createRecursiveScript(const QString& directory, bool interactiveMode) {
// If folder contains a test, then we are at a leaf // If folder contains a test, then we are at a leaf
const QString testPathname{ directory + "/" + TEST_FILENAME }; const QString testPathname{ directory + "/" + TEST_FILENAME };
if (QFileInfo(testPathname).exists()) { if (QFileInfo(testPathname).exists()) {
@ -841,7 +849,10 @@ void Test::createRecursiveScript(const QString& directory, bool interactiveMode)
textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl;
textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl;
textStream << " nitpick.enableRecursive();" << endl; textStream << " nitpick.enableRecursive();" << endl;
textStream << " nitpick.enableAuto();" << endl; textStream << " nitpick.enableAuto();" << endl << endl;
textStream << " if (typeof Test !== 'undefined') {" << endl;
textStream << " Test.wait(10000);" << endl;
textStream << " }" << endl;
textStream << "} else {" << endl; textStream << "} else {" << endl;
textStream << " depth++" << endl; textStream << " depth++" << endl;
textStream << "}" << endl << endl; textStream << "}" << endl << endl;
@ -861,7 +872,7 @@ void Test::createRecursiveScript(const QString& directory, bool interactiveMode)
recursiveTestsFile.close(); recursiveTestsFile.close();
} }
void Test::createTestsOutline() { void TestCreator::createTestsOutline() {
QString previousSelection = _testDirectory; QString previousSelection = _testDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") { if (!parent.isNull() && parent.right(1) != "/") {
@ -887,7 +898,7 @@ void Test::createTestsOutline() {
QTextStream stream(&mdFile); QTextStream stream(&mdFile);
//Test title //TestCreator title
stream << "# Outline of all tests\n"; stream << "# Outline of all tests\n";
stream << "Directories with an appended (*) have an automatic test\n\n"; stream << "Directories with an appended (*) have an automatic test\n\n";
@ -945,10 +956,10 @@ void Test::createTestsOutline() {
mdFile.close(); mdFile.close();
QMessageBox::information(0, "Success", "Test outline file " + testsOutlineFilename + " has been created"); QMessageBox::information(0, "Success", "TestCreator outline file " + testsOutlineFilename + " has been created");
} }
void Test::createTestRailTestCases() { void TestCreator::createTestRailTestCases() {
QString previousSelection = _testDirectory; QString previousSelection = _testDirectory;
QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); QString parent = previousSelection.left(previousSelection.lastIndexOf('/'));
if (!parent.isNull() && parent.right(1) != "/") { if (!parent.isNull() && parent.right(1) != "/") {
@ -985,7 +996,7 @@ void Test::createTestRailTestCases() {
} }
} }
void Test::createTestRailRun() { void TestCreator::createTestRailRun() {
QString outputDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store generated files in", QString outputDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store generated files in",
nullptr, QFileDialog::ShowDirsOnly); nullptr, QFileDialog::ShowDirsOnly);
@ -1001,9 +1012,9 @@ void Test::createTestRailRun() {
_testRailInterface->createTestRailRun(outputDirectory); _testRailInterface->createTestRailRun(outputDirectory);
} }
void Test::updateTestRailRunResult() { void TestCreator::updateTestRailRunResult() {
QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr,
"Zipped Test Results (*.zip)"); "Zipped TestCreator Results (*.zip)");
if (testResults.isNull()) { if (testResults.isNull()) {
return; return;
} }
@ -1022,7 +1033,7 @@ void Test::updateTestRailRunResult() {
_testRailInterface->updateTestRailRunResults(testResults, tempDirectory); _testRailInterface->updateTestRailRunResults(testResults, tempDirectory);
} }
QStringList Test::createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory) { QStringList TestCreator::createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory) {
_imageDirectory = QDir(pathToImageDirectory); _imageDirectory = QDir(pathToImageDirectory);
QStringList nameFilters; QStringList nameFilters;
nameFilters << "*." + imageFormat; nameFilters << "*." + imageFormat;
@ -1034,7 +1045,7 @@ QStringList Test::createListOfAll_imagesInDirectory(const QString& imageFormat,
// Filename (i.e. without extension) contains tests (this is based on all test scripts being within the tests folder) // Filename (i.e. without extension) contains tests (this is based on all test scripts being within the tests folder)
// Last 5 characters in filename are digits (after removing the extension) // Last 5 characters in filename are digits (after removing the extension)
// Extension is 'imageFormat' // Extension is 'imageFormat'
bool Test::isInSnapshotFilenameFormat(const QString& imageFormat, const QString& filename) { bool TestCreator::isInSnapshotFilenameFormat(const QString& imageFormat, const QString& filename) {
bool contains_tests = filename.contains("tests" + PATH_SEPARATOR); bool contains_tests = filename.contains("tests" + PATH_SEPARATOR);
QString filenameWithoutExtension = filename.left(filename.lastIndexOf('.')); QString filenameWithoutExtension = filename.left(filename.lastIndexOf('.'));
@ -1049,7 +1060,7 @@ bool Test::isInSnapshotFilenameFormat(const QString& imageFormat, const QString&
// For a file named "D_GitHub_hifi-tests_tests_content_entity_zone_create_0.jpg", the test directory is // For a file named "D_GitHub_hifi-tests_tests_content_entity_zone_create_0.jpg", the test directory is
// D:/GitHub/hifi-tests/tests/content/entity/zone/create // D:/GitHub/hifi-tests/tests/content/entity/zone/create
// This method assumes the filename is in the correct format // This method assumes the filename is in the correct format
QString Test::getExpectedImageDestinationDirectory(const QString& filename) { QString TestCreator::getExpectedImageDestinationDirectory(const QString& filename) {
QString filenameWithoutExtension = filename.left(filename.length() - 4); QString filenameWithoutExtension = filename.left(filename.length() - 4);
QStringList filenameParts = filenameWithoutExtension.split(PATH_SEPARATOR); QStringList filenameParts = filenameWithoutExtension.split(PATH_SEPARATOR);
@ -1066,7 +1077,7 @@ QString Test::getExpectedImageDestinationDirectory(const QString& filename) {
// is ...tests/content/entity/zone/create // is ...tests/content/entity/zone/create
// This is used to create the full URL // This is used to create the full URL
// This method assumes the filename is in the correct format // This method assumes the filename is in the correct format
QString Test::getExpectedImagePartialSourceDirectory(const QString& filename) { QString TestCreator::getExpectedImagePartialSourceDirectory(const QString& filename) {
QString filenameWithoutExtension = filename.left(filename.length() - 4); QString filenameWithoutExtension = filename.left(filename.length() - 4);
QStringList filenameParts = filenameWithoutExtension.split(PATH_SEPARATOR); QStringList filenameParts = filenameWithoutExtension.split(PATH_SEPARATOR);
@ -1091,13 +1102,18 @@ QString Test::getExpectedImagePartialSourceDirectory(const QString& filename) {
return result; return result;
} }
void Test::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) { void TestCreator::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) {
_testRailCreateMode = testRailCreateMode; _testRailCreateMode = testRailCreateMode;
} }
void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { void TestCreator::createWebPage(
QCheckBox* updateAWSCheckBox,
QRadioButton* diffImageRadioButton,
QRadioButton* ssimImageRadionButton,
QLineEdit* urlLineEdit
) {
QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr,
"Zipped Test Results (TestResults--*.zip)"); "Zipped TestCreator Results (TestResults--*.zip)");
if (testResults.isNull()) { if (testResults.isNull()) {
return; return;
} }
@ -1112,5 +1128,12 @@ void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) {
_awsInterface = new AWSInterface; _awsInterface = new AWSInterface;
} }
_awsInterface->createWebPageFromResults(testResults, workingDirectory, updateAWSCheckBox, urlLineEdit); _awsInterface->createWebPageFromResults(
testResults,
workingDirectory,
updateAWSCheckBox,
diffImageRadioButton,
ssimImageRadionButton,
urlLineEdit
);
} }

View file

@ -1,5 +1,5 @@
// //
// Test.h // TestCreator.h
// //
// Created by Nissim Hadar on 2 Nov 2017. // Created by Nissim Hadar on 2 Nov 2017.
// Copyright 2013 High Fidelity, Inc. // Copyright 2013 High Fidelity, Inc.
@ -8,8 +8,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
// //
#ifndef hifi_test_h #ifndef hifi_testCreator_h
#define hifi_test_h #define hifi_testCreator_h
#include <QtWidgets/QFileDialog> #include <QtWidgets/QFileDialog>
#include <QtWidgets/QMessageBox> #include <QtWidgets/QMessageBox>
@ -18,6 +18,7 @@
#include "AWSInterface.h" #include "AWSInterface.h"
#include "ImageComparer.h" #include "ImageComparer.h"
#include "Downloader.h"
#include "MismatchWindow.h" #include "MismatchWindow.h"
#include "TestRailInterface.h" #include "TestRailInterface.h"
@ -40,9 +41,9 @@ enum TestRailCreateMode {
XML XML
}; };
class Test { class TestCreator {
public: public:
Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode); TestCreator(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode);
void startTestsEvaluation(const bool isRunningFromCommandLine, void startTestsEvaluation(const bool isRunningFromCommandLine,
const bool isRunningInAutomaticTestRun, const bool isRunningInAutomaticTestRun,
@ -87,7 +88,7 @@ public:
void includeTest(QTextStream& textStream, const QString& testPathname); 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); void appendTestResultsToFile(QString testResultFilename, bool hasFailed);
bool createTestResultsFolderPath(const QString& directory); bool createTestResultsFolderPath(const QString& directory);
@ -102,7 +103,11 @@ public:
void setTestRailCreateMode(TestRailCreateMode testRailCreateMode); void setTestRailCreateMode(TestRailCreateMode testRailCreateMode);
void createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit); void createWebPage(
QCheckBox* updateAWSCheckBox,
QRadioButton* diffImageRadioButton,
QRadioButton* ssimImageRadionButton,
QLineEdit* urlLineEdit);
private: private:
QProgressBar* _progressBar; QProgressBar* _progressBar;
@ -116,7 +121,7 @@ private:
const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FOLDER { "TestResults" };
const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" };
const double THRESHOLD{ 0.990 }; const double THRESHOLD{ 0.995 };
QDir _imageDirectory; QDir _imageDirectory;
@ -163,6 +168,7 @@ private:
TestRailCreateMode _testRailCreateMode { PYTHON }; TestRailCreateMode _testRailCreateMode { PYTHON };
AWSInterface* _awsInterface; AWSInterface* _awsInterface;
Downloader* _downloader;
}; };
#endif // hifi_test_h #endif

View file

@ -9,7 +9,7 @@
// //
#include "TestRailInterface.h" #include "TestRailInterface.h"
#include "Test.h" #include "TestCreator.h"
#include <quazip5/quazip.h> #include <quazip5/quazip.h>
#include <quazip5/JlCompress.h> #include <quazip5/JlCompress.h>
@ -258,7 +258,7 @@ bool TestRailInterface::requestTestRailResultsDataFromUser() {
} }
bool TestRailInterface::isAValidTestDirectory(const QString& directory) { bool TestRailInterface::isAValidTestDirectory(const QString& directory) {
if (Test::isAValidDirectory(directory)) { if (TestCreator::isAValidDirectory(directory)) {
// Ignore the utils and preformance directories // Ignore the utils and preformance directories
if (directory.right(QString("utils").length()) == "utils" || if (directory.right(QString("utils").length()) == "utils" ||
directory.right(QString("performance").length()) == "performance") { directory.right(QString("performance").length()) == "performance") {
@ -352,6 +352,7 @@ void TestRailInterface::createAddTestCasesPythonScript(const QString& testDirect
) { ) {
QProcess* process = new QProcess(); QProcess* process = new QProcess();
_busyWindow.setWindowTitle("Updating TestRail");
connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); });
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
@ -482,6 +483,7 @@ void TestRailInterface::addRun() {
QMessageBox::Yes | QMessageBox::No).exec() QMessageBox::Yes | QMessageBox::No).exec()
) { ) {
QProcess* process = new QProcess(); QProcess* process = new QProcess();
_busyWindow.setWindowTitle("Updating TestRail");
connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); });
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
@ -591,6 +593,7 @@ void TestRailInterface::updateRunWithResults() {
QMessageBox::Yes | QMessageBox::No).exec() QMessageBox::Yes | QMessageBox::No).exec()
) { ) {
QProcess* process = new QProcess(); QProcess* process = new QProcess();
_busyWindow.setWindowTitle("Updating TestRail");
connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); });
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, connect(process, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,

View file

@ -14,6 +14,26 @@
#include "Nitpick.h" #include "Nitpick.h"
extern Nitpick* nitpick; extern Nitpick* nitpick;
TestRunner::TestRunner(
QLabel* workingFolderLabel,
QLabel* statusLabel,
QCheckBox* usePreviousInstallationCheckBox,
QCheckBox* runLatest,
QLineEdit* url,
QCheckBox* runFullSuite,
QLineEdit* scriptURL
) {
_workingFolderLabel = workingFolderLabel;
_statusLabel = statusLabel;
_usePreviousInstallationCheckBox = usePreviousInstallationCheckBox;
_runLatest = runLatest;
_url = url;
_runFullSuite = runFullSuite;
_scriptURL = scriptURL;
_downloader = new Downloader();
}
void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) { void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) {
// Everything will be written to this folder // Everything will be written to this folder
QString previousSelection = _workingFolder; QString previousSelection = _workingFolder;
@ -49,7 +69,7 @@ void TestRunner::downloadBuildXml(void* caller) {
urls << DEV_BUILD_XML_URL; urls << DEV_BUILD_XML_URL;
filenames << DEV_BUILD_XML_FILENAME; filenames << DEV_BUILD_XML_FILENAME;
nitpick->downloadFiles(urls, _workingFolder, filenames, caller); _downloader->downloadFiles(urls, _workingFolder, filenames, caller);
} }
void TestRunner::parseBuildInformation() { void TestRunner::parseBuildInformation() {

View file

@ -11,6 +11,8 @@
#ifndef hifi_testRunner_h #ifndef hifi_testRunner_h
#define hifi_testRunner_h #define hifi_testRunner_h
#include "Downloader.h"
#include <QCheckBox> #include <QCheckBox>
#include <QDir> #include <QDir>
#include <QLabel> #include <QLabel>
@ -28,7 +30,18 @@ public:
class TestRunner { class TestRunner {
public: public:
TestRunner(
QLabel* workingFolderLabel,
QLabel* statusLabel,
QCheckBox* usePreviousInstallationOnMobileCheckBox,
QCheckBox* runLatest,
QLineEdit* url,
QCheckBox* runFullSuite,
QLineEdit* scriptURL
);
void setWorkingFolder(QLabel* workingFolderLabel); void setWorkingFolder(QLabel* workingFolderLabel);
void downloadBuildXml(void* caller); void downloadBuildXml(void* caller);
void parseBuildInformation(); void parseBuildInformation();
QString getInstallerNameFromURL(const QString& url); QString getInstallerNameFromURL(const QString& url);
@ -36,10 +49,15 @@ public:
void appendLog(const QString& message); void appendLog(const QString& message);
protected: protected:
Downloader* _downloader;
QLabel* _workingFolderLabel; QLabel* _workingFolderLabel;
QLabel* _statusLabel; QLabel* _statusLabel;
QLineEdit* _url; QCheckBox* _usePreviousInstallationCheckBox;
QCheckBox* _runLatest; QCheckBox* _runLatest;
QLineEdit* _url;
QCheckBox* _runFullSuite;
QLineEdit* _scriptURL;
QString _workingFolder; QString _workingFolder;

View file

@ -27,23 +27,22 @@ TestRunnerDesktop::TestRunnerDesktop(
std::vector<QTimeEdit*> timeEdits, std::vector<QTimeEdit*> timeEdits,
QLabel* workingFolderLabel, QLabel* workingFolderLabel,
QCheckBox* runServerless, QCheckBox* runServerless,
QCheckBox* usePreviousInstallationOnMobileCheckBox,
QCheckBox* runLatest, QCheckBox* runLatest,
QLineEdit* url, QLineEdit* url,
QCheckBox* runFullSuite,
QLineEdit* scriptURL,
QPushButton* runNow, QPushButton* runNow,
QLabel* statusLabel, QLabel* statusLabel,
QObject* parent QObject* parent
) : QObject(parent) ) : QObject(parent), TestRunner(workingFolderLabel, statusLabel, usePreviousInstallationOnMobileCheckBox, runLatest, url, runFullSuite, scriptURL)
{ {
_dayCheckboxes = dayCheckboxes; _dayCheckboxes = dayCheckboxes;
_timeEditCheckboxes = timeEditCheckboxes; _timeEditCheckboxes = timeEditCheckboxes;
_timeEdits = timeEdits; _timeEdits = timeEdits;
_workingFolderLabel = workingFolderLabel;
_runServerless = runServerless; _runServerless = runServerless;
_runLatest = runLatest;
_url = url;
_runNow = runNow; _runNow = runNow;
_statusLabel = statusLabel;
_installerThread = new QThread(); _installerThread = new QThread();
_installerWorker = new InstallerWorker(); _installerWorker = new InstallerWorker();
@ -179,10 +178,14 @@ void TestRunnerDesktop::run() {
// This will be restored at the end of the tests // This will be restored at the end of the tests
saveExistingHighFidelityAppDataFolder(); saveExistingHighFidelityAppDataFolder();
_statusLabel->setText("Downloading Build XML"); if (_usePreviousInstallationCheckBox->isChecked()) {
downloadBuildXml((void*)this); installationComplete();
} else {
_statusLabel->setText("Downloading Build XML");
downloadBuildXml((void*)this);
// `downloadComplete` will run after download has completed downloadComplete();
}
} }
void TestRunnerDesktop::downloadComplete() { void TestRunnerDesktop::downloadComplete() {
@ -209,9 +212,9 @@ void TestRunnerDesktop::downloadComplete() {
_statusLabel->setText("Downloading installer"); _statusLabel->setText("Downloading installer");
nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); _downloader->downloadFiles(urls, _workingFolder, filenames, (void*)this);
// `downloadComplete` will run again after download has completed downloadComplete();
} else { } else {
// Download of Installer has completed // Download of Installer has completed
@ -292,15 +295,19 @@ void TestRunnerDesktop::installationComplete() {
void TestRunnerDesktop::verifyInstallationSucceeded() { void TestRunnerDesktop::verifyInstallationSucceeded() {
// Exit if the executables are missing. // Exit if the executables are missing.
// On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe"); QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe");
QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe"); QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe");
QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe"); QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe");
if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) { if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) {
QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled"); if (_runLatest->isChecked()) {
exit(-1); // On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error
QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled");
exit(-1);
} else {
QMessageBox::critical(0, "Installation of High Fidelity not found", "Please verify that working folder contains a proper installation");
}
} }
#endif #endif
} }
@ -457,8 +464,9 @@ void TestRunnerDesktop::runInterfaceWithTestScript() {
QString deleteScript = QString deleteScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js"; QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js";
QString testScript = QString testScript = (_runFullSuite->isChecked())
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; ? QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"
: _scriptURL->text();
QString commandLine; QString commandLine;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
@ -537,15 +545,16 @@ void TestRunnerDesktop::runInterfaceWithTestScript() {
} }
void TestRunnerDesktop::interfaceExecutionComplete() { void TestRunnerDesktop::interfaceExecutionComplete() {
QThread::msleep(500);
QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt"); QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt");
if (!testCompleted.exists()) { if (!testCompleted.exists()) {
QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated"); QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated");
} }
killProcesses();
evaluateResults(); evaluateResults();
killProcesses();
// The High Fidelity AppData folder will be restored after evaluation has completed // The High Fidelity AppData folder will be restored after evaluation has completed
} }
@ -591,7 +600,6 @@ void TestRunnerDesktop::addBuildNumberToResults(const QString& zippedFolderName)
if (!QFile::rename(zippedFolderName, augmentedFilename)) { if (!QFile::rename(zippedFolderName, augmentedFilename)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Could not rename '" + zippedFolderName + "' to '" + augmentedFilename); QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Could not rename '" + zippedFolderName + "' to '" + augmentedFilename);
exit(-1); exit(-1);
} }
} }
@ -667,6 +675,13 @@ void TestRunnerDesktop::checkTime() {
QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) { QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) {
try { try {
QStringList urlParts = url.split("/"); QStringList urlParts = url.split("/");
if (urlParts.size() <= 2) {
#ifdef Q_OS_WIN
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`";
#elif defined Q_OS_MAC
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`";
#endif
}
QStringList filenameParts = urlParts[urlParts.size() - 1].split("-"); QStringList filenameParts = urlParts[urlParts.size() - 1].split("-");
if (filenameParts.size() <= 3) { if (filenameParts.size() <= 3) {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN

View file

@ -12,7 +12,6 @@
#define hifi_testRunnerDesktop_h #define hifi_testRunnerDesktop_h
#include <QDir> #include <QDir>
#include <QLabel>
#include <QObject> #include <QObject>
#include <QPushButton> #include <QPushButton>
#include <QThread> #include <QThread>
@ -32,8 +31,11 @@ public:
std::vector<QTimeEdit*> timeEdits, std::vector<QTimeEdit*> timeEdits,
QLabel* workingFolderLabel, QLabel* workingFolderLabel,
QCheckBox* runServerless, QCheckBox* runServerless,
QCheckBox* usePreviousInstallationOnMobileCheckBox,
QCheckBox* runLatest, QCheckBox* runLatest,
QLineEdit* url, QLineEdit* url,
QCheckBox* runFullSuite,
QLineEdit* scriptURL,
QPushButton* runNow, QPushButton* runNow,
QLabel* statusLabel, QLabel* statusLabel,
@ -99,7 +101,6 @@ private:
std::vector<QCheckBox*> _dayCheckboxes; std::vector<QCheckBox*> _dayCheckboxes;
std::vector<QCheckBox*> _timeEditCheckboxes; std::vector<QCheckBox*> _timeEditCheckboxes;
std::vector<QTimeEdit*> _timeEdits; std::vector<QTimeEdit*> _timeEdits;
QLabel* _workingFolderLabel;
QCheckBox* _runServerless; QCheckBox* _runServerless;
QPushButton* _runNow; QPushButton* _runNow;
QTimer* _timer; QTimer* _timer;

View file

@ -25,14 +25,16 @@ TestRunnerMobile::TestRunnerMobile(
QPushButton* downloadAPKPushbutton, QPushButton* downloadAPKPushbutton,
QPushButton* installAPKPushbutton, QPushButton* installAPKPushbutton,
QPushButton* runInterfacePushbutton, QPushButton* runInterfacePushbutton,
QCheckBox* usePreviousInstallationOnMobileCheckBox,
QCheckBox* runLatest, QCheckBox* runLatest,
QLineEdit* url, QLineEdit* url,
QCheckBox* runFullSuite,
QLineEdit* scriptURL,
QLabel* statusLabel, QLabel* statusLabel,
QObject* parent QObject* parent
) : QObject(parent), _adbInterface(NULL) ) : QObject(parent), TestRunner(workingFolderLabel, statusLabel, usePreviousInstallationOnMobileCheckBox, runLatest, url, runFullSuite, scriptURL)
{ {
_workingFolderLabel = workingFolderLabel;
_connectDeviceButton = connectDeviceButton; _connectDeviceButton = connectDeviceButton;
_pullFolderButton = pullFolderButton; _pullFolderButton = pullFolderButton;
_detectedDeviceLabel = detectedDeviceLabel; _detectedDeviceLabel = detectedDeviceLabel;
@ -40,13 +42,15 @@ TestRunnerMobile::TestRunnerMobile(
_downloadAPKPushbutton = downloadAPKPushbutton; _downloadAPKPushbutton = downloadAPKPushbutton;
_installAPKPushbutton = installAPKPushbutton; _installAPKPushbutton = installAPKPushbutton;
_runInterfacePushbutton = runInterfacePushbutton; _runInterfacePushbutton = runInterfacePushbutton;
_runLatest = runLatest;
_url = url;
_statusLabel = statusLabel;
folderLineEdit->setText("/sdcard/DCIM/TEST"); folderLineEdit->setText("/sdcard/DCIM/TEST");
modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; modelNames["SM_G955U1"] = "Samsung S8+ unlocked";
modelNames["SM_N960U1"] = "Samsung Note 9 unlocked";
modelNames["SM_T380"] = "Samsung Tab A";
modelNames["Quest"] = "Quest";
_adbInterface = NULL;
} }
TestRunnerMobile::~TestRunnerMobile() { TestRunnerMobile::~TestRunnerMobile() {
@ -66,6 +70,7 @@ void TestRunnerMobile::connectDevice() {
QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString devicesFullFilename{ _workingFolder + "/devices.txt" };
QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename;
appendLog(command);
system(command.toStdString().c_str()); system(command.toStdString().c_str());
if (!QFile::exists(devicesFullFilename)) { if (!QFile::exists(devicesFullFilename)) {
@ -93,7 +98,7 @@ void TestRunnerMobile::connectDevice() {
QString deviceID = tokens[0]; QString deviceID = tokens[0];
QString modelID = tokens[3].split(':')[1]; QString modelID = tokens[3].split(':')[1];
QString modelName = "UKNOWN"; QString modelName = "UNKNOWN";
if (modelNames.count(modelID) == 1) { if (modelNames.count(modelID) == 1) {
modelName = modelNames[modelID]; modelName = modelNames[modelID];
} }
@ -102,6 +107,8 @@ void TestRunnerMobile::connectDevice() {
_pullFolderButton->setEnabled(true); _pullFolderButton->setEnabled(true);
_folderLineEdit->setEnabled(true); _folderLineEdit->setEnabled(true);
_downloadAPKPushbutton->setEnabled(true); _downloadAPKPushbutton->setEnabled(true);
_installAPKPushbutton->setEnabled(true);
_runInterfacePushbutton->setEnabled(true);
} }
} }
#endif #endif
@ -109,6 +116,8 @@ void TestRunnerMobile::connectDevice() {
void TestRunnerMobile::downloadAPK() { void TestRunnerMobile::downloadAPK() {
downloadBuildXml((void*)this); downloadBuildXml((void*)this);
downloadComplete();
} }
@ -141,11 +150,12 @@ void TestRunnerMobile::downloadComplete() {
_statusLabel->setText("Downloading installer"); _statusLabel->setText("Downloading installer");
nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); _downloader->downloadFiles(urls, _workingFolder, filenames, (void*)this);
} else { } else {
_statusLabel->setText("Installer download complete"); _statusLabel->setText("Installer download complete");
_installAPKPushbutton->setEnabled(true);
} }
_installAPKPushbutton->setEnabled(true);
} }
void TestRunnerMobile::installAPK() { void TestRunnerMobile::installAPK() {
@ -154,11 +164,25 @@ void TestRunnerMobile::installAPK() {
_adbInterface = new AdbInterface(); _adbInterface = new AdbInterface();
} }
if (_installerFilename.isNull()) {
QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder,
"Available APKs (*.apk)"
);
if (installerPathname.isNull()) {
return;
}
// Remove the path
QStringList parts = installerPathname.split('/');
_installerFilename = parts[parts.length() - 1];
}
_statusLabel->setText("Installing"); _statusLabel->setText("Installing");
QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt";
appendLog(command);
system(command.toStdString().c_str()); system(command.toStdString().c_str());
_statusLabel->setText("Installation complete"); _statusLabel->setText("Installation complete");
_runInterfacePushbutton->setEnabled(true);
#endif #endif
} }
@ -169,7 +193,22 @@ void TestRunnerMobile::runInterface() {
} }
_statusLabel->setText("Starting Interface"); _statusLabel->setText("Starting Interface");
QString command = _adbInterface->getAdbCommand() + " shell monkey -p io.highfidelity.hifiinterface -v 1";
QString testScript = (_runFullSuite->isChecked())
? QString("https://raw.githubusercontent.com/") + nitpick->getSelectedUser() + "/hifi_tests/" + nitpick->getSelectedBranch() + "/tests/testRecursive.js"
: _scriptURL->text();
QString command = _adbInterface->getAdbCommand() +
" shell am start -n io.highfidelity.hifiinterface/.PermissionChecker" +
" --es args \\\"" +
" --url file:///~/serverless/tutorial.json" +
" --no-updater" +
" --no-login-suggestion" +
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation /sdcard/snapshots" +
"\\\"";
appendLog(command);
system(command.toStdString().c_str()); system(command.toStdString().c_str());
_statusLabel->setText("Interface started"); _statusLabel->setText("Interface started");
#endif #endif
@ -182,7 +221,8 @@ void TestRunnerMobile::pullFolder() {
} }
_statusLabel->setText("Pulling folder"); _statusLabel->setText("Pulling folder");
QString command = _adbInterface->getAdbCommand() + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename; QString command = _adbInterface->getAdbCommand() + " pull " + _folderLineEdit->text() + " " + _workingFolder;
appendLog(command);
system(command.toStdString().c_str()); system(command.toStdString().c_str());
_statusLabel->setText("Pull complete"); _statusLabel->setText("Pull complete");
#endif #endif

View file

@ -12,7 +12,6 @@
#define hifi_testRunnerMobile_h #define hifi_testRunnerMobile_h
#include <QMap> #include <QMap>
#include <QLabel>
#include <QObject> #include <QObject>
#include <QPushButton> #include <QPushButton>
@ -31,8 +30,11 @@ public:
QPushButton* downloadAPKPushbutton, QPushButton* downloadAPKPushbutton,
QPushButton* installAPKPushbutton, QPushButton* installAPKPushbutton,
QPushButton* runInterfacePushbutton, QPushButton* runInterfacePushbutton,
QCheckBox* usePreviousInstallationOnMobileCheckBox,
QCheckBox* runLatest, QCheckBox* runLatest,
QLineEdit* url, QLineEdit* url,
QCheckBox* runFullSuite,
QLineEdit* scriptURL,
QLabel* statusLabel, QLabel* statusLabel,
QObject* parent = 0 QObject* parent = 0

View file

@ -10,21 +10,38 @@
#ifndef hifi_common_h #ifndef hifi_common_h
#define hifi_common_h #define hifi_common_h
#include <vector>
#include <QtCore/QString> #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 { class TestResult {
public: 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), _error(error),
_pathname(pathname), _pathname(pathname),
_expectedImageFilename(expectedImageFilename), _expectedImageFilename(expectedImageFilename),
_actualImageFilename(actualImageFilename) _actualImageFilename(actualImageFilename),
_ssimResults(ssimResults)
{} {}
double _error; double _error;
QString _pathname; QString _pathname;
QString _expectedImageFilename; QString _expectedImageFilename;
QString _actualImageFilename; QString _actualImageFilename;
SSIMResults _ssimResults;
}; };
enum UserResponse { enum UserResponse {

View file

@ -34,6 +34,9 @@
</property> </property>
</widget> </widget>
<widget class="QTabWidget" name="tabWidget"> <widget class="QTabWidget" name="tabWidget">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>45</x> <x>45</x>
@ -43,7 +46,7 @@
</rect> </rect>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>5</number>
</property> </property>
<widget class="QWidget" name="tab_1"> <widget class="QWidget" name="tab_1">
<attribute name="title"> <attribute name="title">
@ -495,7 +498,7 @@
<widget class="QCheckBox" name="checkBoxServerless"> <widget class="QCheckBox" name="checkBoxServerless">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>20</x> <x>240</x>
<y>70</y> <y>70</y>
<width>120</width> <width>120</width>
<height>20</height> <height>20</height>
@ -549,13 +552,80 @@
</property> </property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>170</x> <x>175</x>
<y>100</y> <y>100</y>
<width>451</width> <width>445</width>
<height>21</height> <height>21</height>
</rect> </rect>
</property> </property>
</widget> </widget>
<widget class="QLabel" name="workingFolderLabel_5">
<property name="geometry">
<rect>
<x>128</x>
<y>125</y>
<width>40</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>Script</string>
</property>
</widget>
<widget class="QLineEdit" name="scriptURLOnDesktopLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>175</x>
<y>130</y>
<width>445</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QCheckBox" name="runFullSuiteOnDesktopCheckBox">
<property name="geometry">
<rect>
<x>20</x>
<y>130</y>
<width>120</width>
<height>20</height>
</rect>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If unchecked, will not show results during evaluation&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Run Full Suite</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
<widget class="QCheckBox" name="usePreviousInstallationOnDesktopCheckBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>20</x>
<y>70</y>
<width>171</width>
<height>20</height>
</rect>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If unchecked, will not show results during evaluation&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>usePreviousInstallation</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</widget> </widget>
<widget class="QWidget" name="tab_5"> <widget class="QWidget" name="tab_5">
<attribute name="title"> <attribute name="title">
@ -568,7 +638,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>90</y> <y>150</y>
<width>160</width> <width>160</width>
<height>30</height> <height>30</height>
</rect> </rect>
@ -581,7 +651,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>190</x> <x>190</x>
<y>96</y> <y>156</y>
<width>320</width> <width>320</width>
<height>30</height> <height>30</height>
</rect> </rect>
@ -623,7 +693,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>460</x> <x>460</x>
<y>410</y> <y>440</y>
<width>160</width> <width>160</width>
<height>30</height> <height>30</height>
</rect> </rect>
@ -639,7 +709,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>410</y> <y>440</y>
<width>440</width> <width>440</width>
<height>30</height> <height>30</height>
</rect> </rect>
@ -651,9 +721,9 @@
</property> </property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>170</x> <x>175</x>
<y>170</y> <y>245</y>
<width>451</width> <width>445</width>
<height>21</height> <height>21</height>
</rect> </rect>
</property> </property>
@ -662,7 +732,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>20</x> <x>20</x>
<y>170</y> <y>245</y>
<width>120</width> <width>120</width>
<height>20</height> <height>20</height>
</rect> </rect>
@ -684,7 +754,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>210</y> <y>100</y>
<width>160</width> <width>160</width>
<height>30</height> <height>30</height>
</rect> </rect>
@ -696,7 +766,7 @@
<widget class="QLabel" name="workingFolderLabel_4"> <widget class="QLabel" name="workingFolderLabel_4">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>300</x> <x>20</x>
<y>60</y> <y>60</y>
<width>41</width> <width>41</width>
<height>31</height> <height>31</height>
@ -709,7 +779,7 @@
<widget class="QLabel" name="statusLabelOnMobile"> <widget class="QLabel" name="statusLabelOnMobile">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>350</x> <x>70</x>
<y>60</y> <y>60</y>
<width>271</width> <width>271</width>
<height>31</height> <height>31</height>
@ -726,7 +796,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>250</y> <y>325</y>
<width>160</width> <width>160</width>
<height>30</height> <height>30</height>
</rect> </rect>
@ -742,7 +812,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>300</y> <y>375</y>
<width>160</width> <width>160</width>
<height>30</height> <height>30</height>
</rect> </rect>
@ -751,6 +821,86 @@
<string>Run Interface</string> <string>Run Interface</string>
</property> </property>
</widget> </widget>
<widget class="QLabel" name="workingFolderLabel_6">
<property name="geometry">
<rect>
<x>140</x>
<y>240</y>
<width>31</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>URL</string>
</property>
</widget>
<widget class="QLineEdit" name="scriptURLOnMobileLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>175</x>
<y>275</y>
<width>445</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QLabel" name="workingFolderLabel_7">
<property name="geometry">
<rect>
<x>140</x>
<y>270</y>
<width>40</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>Script</string>
</property>
</widget>
<widget class="QCheckBox" name="runFullSuiteOnMobileCheckBox">
<property name="geometry">
<rect>
<x>20</x>
<y>275</y>
<width>120</width>
<height>20</height>
</rect>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If unchecked, will not show results during evaluation&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Run Full Suite</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
<widget class="QCheckBox" name="usePreviousInstallationOnMobileCheckBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>20</x>
<y>210</y>
<width>171</width>
<height>20</height>
</rect>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If unchecked, will not show results during evaluation&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>usePreviousInstallation</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</widget> </widget>
<widget class="QWidget" name="tab_2"> <widget class="QWidget" name="tab_2">
<attribute name="title"> <attribute name="title">
@ -760,7 +910,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>190</x> <x>190</x>
<y>180</y> <y>200</y>
<width>131</width> <width>131</width>
<height>20</height> <height>20</height>
</rect> </rect>
@ -776,7 +926,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>330</x> <x>330</x>
<y>170</y> <y>190</y>
<width>181</width> <width>181</width>
<height>51</height> <height>51</height>
</rect> </rect>
@ -889,8 +1039,8 @@
</property> </property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>270</x> <x>370</x>
<y>30</y> <y>20</y>
<width>160</width> <width>160</width>
<height>51</height> <height>51</height>
</rect> </rect>
@ -921,6 +1071,41 @@
<height>21</height> <height>21</height>
</rect> </rect>
</property> </property>
<property name="readOnly">
<bool>true</bool>
</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>
</widget> </widget>
<zorder>groupBox</zorder> <zorder>groupBox</zorder>