diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 65e0acd4a6..aaf58a7f42 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -27,7 +28,7 @@ const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; -const unsigned int AVATAR_DATA_SEND_INTERVAL_USECS = (1 / 60.0) * 1000 * 1000; +const unsigned int AVATAR_DATA_SEND_INTERVAL_MSECS = (1.0f / 60.0f) * 1000; AvatarMixer::AvatarMixer(const QByteArray& packet) : ThreadedAssignment(packet), @@ -54,43 +55,97 @@ const float BILLBOARD_AND_IDENTITY_SEND_PROBABILITY = 1.0f / 300.0f; // NOTE: some additional optimizations to consider. // 1) use the view frustum to cull those avatars that are out of view. Since avatar data doesn't need to be present // if the avatar is not in view or in the keyhole. -// 2) after culling for view frustum, sort order the avatars by distance, send the closest ones first. -// 3) if we need to rate limit the amount of data we send, we can use a distance weighted "semi-random" function to -// determine which avatars are included in the packet stream -// 4) we should optimize the avatar data format to be more compact (100 bytes is pretty wasteful). void AvatarMixer::broadcastAvatarData() { + + int idleTime = QDateTime::currentMSecsSinceEpoch() - _lastFrameTimestamp; + + ++_numStatFrames; + + const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; + const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; + + const float RATIO_BACK_OFF = 0.02f; + + const int TRAILING_AVERAGE_FRAMES = 100; + int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES; + + const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; + const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; + + _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) + + (idleTime * CURRENT_FRAME_RATIO / (float) AVATAR_DATA_SEND_INTERVAL_MSECS); + + float lastCutoffRatio = _performanceThrottlingRatio; + bool hasRatioChanged = false; + + if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { + if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { + // we're struggling - change our min required loudness to reduce some load + _performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio)); + + qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" + << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; + hasRatioChanged = true; + } else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) { + // we've recovered and can back off the required loudness + _performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF; + + if (_performanceThrottlingRatio < 0) { + _performanceThrottlingRatio = 0; + } + + qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" + << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; + hasRatioChanged = true; + } + + if (hasRatioChanged) { + framesSinceCutoffEvent = 0; + } + } + + if (!hasRatioChanged) { + ++framesSinceCutoffEvent; + } + static QByteArray mixedAvatarByteArray; int numPacketHeaderBytes = populatePacketHeader(mixedAvatarByteArray, PacketTypeBulkAvatarData); NodeList* nodeList = NodeList::getInstance(); + AvatarMixerClientData* nodeData = NULL; + AvatarMixerClientData* otherNodeData = NULL; + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { - if (node->getLinkedData() && node->getType() == NodeType::Agent && node->getActiveSocket()) { + if (node->getLinkedData() && node->getType() == NodeType::Agent && node->getActiveSocket() + && (nodeData = reinterpret_cast(node->getLinkedData()))->getMutex().tryLock()) { ++_sumListeners; // reset packet pointers for this node mixedAvatarByteArray.resize(numPacketHeaderBytes); - AvatarMixerClientData* myData = reinterpret_cast(node->getLinkedData()); - AvatarData& avatar = myData->getAvatar(); + AvatarData& avatar = nodeData->getAvatar(); glm::vec3 myPosition = avatar.getPosition(); // this is an AGENT we have received head data from // send back a packet with other active node data to this node foreach (const SharedNodePointer& otherNode, nodeList->getNodeHash()) { - if (otherNode->getLinkedData() && otherNode->getUUID() != node->getUUID()) { + if (otherNode->getLinkedData() && otherNode->getUUID() != node->getUUID() + && (otherNodeData = reinterpret_cast(otherNode->getLinkedData()))->getMutex().tryLock()) { AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); AvatarData& otherAvatar = otherNodeData->getAvatar(); glm::vec3 otherPosition = otherAvatar.getPosition(); + float distanceToAvatar = glm::length(myPosition - otherPosition); // The full rate distance is the distance at which EVERY update will be sent for this avatar // at a distance of twice the full rate distance, there will be a 50% chance of sending this avatar's update const float FULL_RATE_DISTANCE = 2.f; + // Decide whether to send this avatar's data based on it's distance from us - if ((distanceToAvatar == 0.f) || (randFloat() < FULL_RATE_DISTANCE / distanceToAvatar) - * (1 - _performanceThrottlingRatio)) { + if ((_performanceThrottlingRatio == 0 || randFloat() < (1.0f - _performanceThrottlingRatio)) + && (distanceToAvatar == 0.f || randFloat() < FULL_RATE_DISTANCE / distanceToAvatar)) { QByteArray avatarByteArray; avatarByteArray.append(otherNode->getUUID().toRfc4122()); avatarByteArray.append(otherAvatar.toByteArray()); @@ -107,7 +162,7 @@ void AvatarMixer::broadcastAvatarData() { // if the receiving avatar has just connected make sure we send out the mesh and billboard // for this avatar (assuming they exist) - bool forceSend = !myData->checkAndSetHasReceivedFirstPackets(); + bool forceSend = !nodeData->checkAndSetHasReceivedFirstPackets(); // we will also force a send of billboard or identity packet // if either has changed in the last frame @@ -140,10 +195,14 @@ void AvatarMixer::broadcastAvatarData() { ++_sumIdentityPackets; } } + + otherNodeData->getMutex().unlock(); } } nodeList->writeDatagram(mixedAvatarByteArray, node); + + nodeData->getMutex().unlock(); } } @@ -187,6 +246,7 @@ void AvatarMixer::readPendingDatagrams() { // parse the identity packet and update the change timestamp if appropriate if (avatar.hasIdentityChangedAfterParsing(receivedPacket)) { + QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->setIdentityChangeTimestamp(QDateTime::currentMSecsSinceEpoch()); } } @@ -203,6 +263,7 @@ void AvatarMixer::readPendingDatagrams() { // parse the billboard packet and update the change timestamp if appropriate if (avatar.hasBillboardChangedAfterParsing(receivedPacket)) { + QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->setBillboardChangeTimestamp(QDateTime::currentMSecsSinceEpoch()); } @@ -248,80 +309,18 @@ void AvatarMixer::run() { nodeList->linkedDataCreateCallback = attachAvatarDataToNode; - int nextFrame = 0; - timeval startTime; + // create a thead for broadcast of avatar data + QThread* broadcastThread = new QThread(this); - gettimeofday(&startTime, NULL); + // setup the timer that will be fired on the broadcast thread + QTimer* broadcastTimer = new QTimer(); + broadcastTimer->setInterval(AVATAR_DATA_SEND_INTERVAL_MSECS); + broadcastTimer->moveToThread(broadcastThread); - int usecToSleep = AVATAR_DATA_SEND_INTERVAL_USECS; + // connect appropriate signals and slots + connect(broadcastTimer, &QTimer::timeout, this, &AvatarMixer::broadcastAvatarData, Qt::DirectConnection); + connect(broadcastThread, SIGNAL(started()), broadcastTimer, SLOT(start())); - const int TRAILING_AVERAGE_FRAMES = 100; - int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES; - - while (!_isFinished) { - - ++_numStatFrames; - - const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; - const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; - - const float RATIO_BACK_OFF = 0.02f; - - const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; - const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; - - if (usecToSleep < 0) { - usecToSleep = 0; - } - - _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) - + (usecToSleep * CURRENT_FRAME_RATIO / (float) AVATAR_DATA_SEND_INTERVAL_USECS); - - float lastCutoffRatio = _performanceThrottlingRatio; - bool hasRatioChanged = false; - - if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { - if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { - // we're struggling - change our min required loudness to reduce some load - _performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio)); - - qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" - << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; - hasRatioChanged = true; - } else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) { - // we've recovered and can back off the required loudness - _performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF; - - if (_performanceThrottlingRatio < 0) { - _performanceThrottlingRatio = 0; - } - - qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" - << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; - hasRatioChanged = true; - } - - if (hasRatioChanged) { - framesSinceCutoffEvent = 0; - } - } - - if (!hasRatioChanged) { - ++framesSinceCutoffEvent; - } - - broadcastAvatarData(); - - QCoreApplication::processEvents(); - - if (_isFinished) { - break; - } - - usecToSleep = usecTimestamp(&startTime) + (++nextFrame * AVATAR_DATA_SEND_INTERVAL_USECS) - usecTimestampNow(); - - if (usecToSleep > 0) { - usleep(usecToSleep); - } - } + // start the broadcastThread + broadcastThread->start(); } diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index eae3c2eb8c..722b21844f 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -3,4 +3,5 @@ Script.include("lookWithTouch.js"); Script.include("editVoxels.js"); Script.include("selectAudioDevice.js"); -Script.include("hydraMove.js"); \ No newline at end of file +Script.include("hydraMove.js"); +Script.include("inspect.js"); \ No newline at end of file diff --git a/examples/lookWithTouch.js b/examples/lookWithTouch.js index 852573aea6..4406b4567e 100644 --- a/examples/lookWithTouch.js +++ b/examples/lookWithTouch.js @@ -9,6 +9,7 @@ // // +var startedTouching = false; var lastX = 0; var lastY = 0; var yawFromMouse = 0; @@ -21,12 +22,14 @@ function touchBeginEvent(event) { } lastX = event.x; lastY = event.y; + startedTouching = true; } function touchEndEvent(event) { if (wantDebugging) { print("touchEndEvent event.x,y=" + event.x + ", " + event.y); } + startedTouching = false; } function touchUpdateEvent(event) { @@ -44,24 +47,26 @@ function touchUpdateEvent(event) { } function update(deltaTime) { - // rotate body yaw for yaw received from mouse - var newOrientation = Quat.multiply(MyAvatar.orientation, Quat.fromPitchYawRollRadians(0, yawFromMouse, 0)); - if (wantDebugging) { - print("changing orientation" - + " [old]MyAvatar.orientation="+MyAvatar.orientation.x + "," + MyAvatar.orientation.y + "," - + MyAvatar.orientation.z + "," + MyAvatar.orientation.w - + " newOrientation="+newOrientation.x + "," + newOrientation.y + "," + newOrientation.z + "," + newOrientation.w); - } - MyAvatar.orientation = newOrientation; - yawFromMouse = 0; + if (startedTouching) { + // rotate body yaw for yaw received from mouse + var newOrientation = Quat.multiply(MyAvatar.orientation, Quat.fromPitchYawRollRadians(0, yawFromMouse, 0)); + if (wantDebugging) { + print("changing orientation" + + " [old]MyAvatar.orientation="+MyAvatar.orientation.x + "," + MyAvatar.orientation.y + "," + + MyAvatar.orientation.z + "," + MyAvatar.orientation.w + + " newOrientation="+newOrientation.x + "," + newOrientation.y + "," + newOrientation.z + "," + newOrientation.w); + } + MyAvatar.orientation = newOrientation; + yawFromMouse = 0; - // apply pitch from mouse - var newPitch = MyAvatar.headPitch + pitchFromMouse; - if (wantDebugging) { - print("changing pitch [old]MyAvatar.headPitch="+MyAvatar.headPitch+ " newPitch="+newPitch); + // apply pitch from mouse + var newPitch = MyAvatar.headPitch + pitchFromMouse; + if (wantDebugging) { + print("changing pitch [old]MyAvatar.headPitch="+MyAvatar.headPitch+ " newPitch="+newPitch); + } + MyAvatar.headPitch = newPitch; + pitchFromMouse = 0; } - MyAvatar.headPitch = newPitch; - pitchFromMouse = 0; } // Map the mouse events to our functions @@ -77,10 +82,6 @@ function scriptEnding() { Controller.releaseTouchEvents(); } -MyAvatar.bodyYaw = 0; -MyAvatar.bodyPitch = 0; -MyAvatar.bodyRoll = 0; - // would be nice to change to update Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 9bd00a0019..c0834aad9d 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -235,7 +235,6 @@ void ScriptEngine::init() { // let the VoxelPacketSender know how frequently we plan to call it _voxelsScriptingInterface.getVoxelPacketSender()->setProcessCallIntervalHint(SCRIPT_DATA_CALLBACK_USECS); _particlesScriptingInterface.getParticlePacketSender()->setProcessCallIntervalHint(SCRIPT_DATA_CALLBACK_USECS); - } void ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { diff --git a/libraries/shared/src/NodeData.cpp b/libraries/shared/src/NodeData.cpp index a7fa18f409..e3800f8b93 100644 --- a/libraries/shared/src/NodeData.cpp +++ b/libraries/shared/src/NodeData.cpp @@ -8,6 +8,12 @@ #include "NodeData.h" +NodeData::NodeData() : + _mutex() +{ + +} + NodeData::~NodeData() { } \ No newline at end of file diff --git a/libraries/shared/src/NodeData.h b/libraries/shared/src/NodeData.h index cf800fc3cd..b6b75443a2 100644 --- a/libraries/shared/src/NodeData.h +++ b/libraries/shared/src/NodeData.h @@ -9,6 +9,7 @@ #ifndef hifi_NodeData_h #define hifi_NodeData_h +#include #include class Node; @@ -16,9 +17,14 @@ class Node; class NodeData : public QObject { Q_OBJECT public: - + NodeData(); virtual ~NodeData() = 0; virtual int parseData(const QByteArray& packet) = 0; + + QMutex& getMutex() { return _mutex; } + +private: + QMutex _mutex; }; #endif diff --git a/libraries/shared/src/NodeList.cpp b/libraries/shared/src/NodeList.cpp index 95417f4f71..e80f25709a 100644 --- a/libraries/shared/src/NodeList.cpp +++ b/libraries/shared/src/NodeList.cpp @@ -171,8 +171,13 @@ qint64 NodeList::writeDatagram(const QByteArray& datagram, const HifiSockAddr& d ++_numCollectedPackets; _numCollectedBytes += datagram.size(); - return _nodeSocket.writeDatagram(datagramCopy, destinationSockAddr.getAddress(), destinationSockAddr.getPort()); + qint64 bytesWritten = _nodeSocket.writeDatagram(datagramCopy, destinationSockAddr.getAddress(), destinationSockAddr.getPort()); + if (bytesWritten < 0) { + qDebug() << "ERROR in writeDatagram:" << _nodeSocket.error() << "-" << _nodeSocket.errorString(); + } + + return bytesWritten; } qint64 NodeList::writeDatagram(const QByteArray& datagram, const SharedNodePointer& destinationNode, @@ -312,6 +317,8 @@ int NodeList::updateNodeWithDataFromPacket(const SharedNodePointer& matchingNode linkedDataCreateCallback(matchingNode.data()); } + QMutexLocker linkedDataLocker(&matchingNode->getLinkedData()->getMutex()); + return matchingNode->getLinkedData()->parseData(packet); } @@ -413,9 +420,8 @@ void NodeList::sendSTUNRequest() { // transaction ID (random 12-byte unsigned integer) const uint NUM_TRANSACTION_ID_BYTES = 12; - unsigned char transactionID[NUM_TRANSACTION_ID_BYTES]; - loadRandomIdentifier(transactionID, NUM_TRANSACTION_ID_BYTES); - memcpy(stunRequestPacket + packetIndex, &transactionID, sizeof(transactionID)); + QUuid randomUUID = QUuid::createUuid(); + memcpy(stunRequestPacket + packetIndex, randomUUID.toRfc4122().data(), NUM_TRANSACTION_ID_BYTES); // lookup the IP for the STUN server static HifiSockAddr stunSockAddr(STUN_SERVER_HOSTNAME, STUN_SERVER_PORT); diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 882d4719c8..efd5180d03 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -206,15 +206,6 @@ bool isInEnvironment(const char* environment) { } } -void loadRandomIdentifier(unsigned char* identifierBuffer, int numBytes) { - // seed the the random number generator - srand(time(NULL)); - - for (int i = 0; i < numBytes; i++) { - identifierBuffer[i] = rand() % 256; - } -} - ////////////////////////////////////////////////////////////////////////////////////////// // Function: getCmdOption() // Description: Handy little function to tell you if a command line flag and option was diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 439b85aa54..d8d686c63b 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -96,8 +96,6 @@ int getNthBit(unsigned char byte, int ordinal); /// determines the bit placement bool isInEnvironment(const char* environment); -void loadRandomIdentifier(unsigned char* identifierBuffer, int numBytes); - const char* getCmdOption(int argc, const char * argv[],const char* option); bool cmdOptionExists(int argc, const char * argv[],const char* option); diff --git a/libraries/shared/src/ThreadedAssignment.cpp b/libraries/shared/src/ThreadedAssignment.cpp index 642f471cc5..be49b18055 100644 --- a/libraries/shared/src/ThreadedAssignment.cpp +++ b/libraries/shared/src/ThreadedAssignment.cpp @@ -46,10 +46,6 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy connect(domainServerTimer, SIGNAL(timeout()), this, SLOT(checkInWithDomainServerOrExit())); domainServerTimer->start(DOMAIN_SERVER_CHECK_IN_USECS / 1000); - QTimer* pingNodesTimer = new QTimer(this); - connect(pingNodesTimer, SIGNAL(timeout()), nodeList, SLOT(pingInactiveNodes())); - pingNodesTimer->start(PING_INACTIVE_NODE_INTERVAL_USECS / 1000); - QTimer* silentNodeRemovalTimer = new QTimer(this); connect(silentNodeRemovalTimer, SIGNAL(timeout()), nodeList, SLOT(removeSilentNodes())); silentNodeRemovalTimer->start(NODE_SILENCE_THRESHOLD_USECS / 1000); @@ -84,7 +80,6 @@ void ThreadedAssignment::checkInWithDomainServerOrExit() { if (NodeList::getInstance()->getNumNoReplyDomainCheckIns() == MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { setFinished(true); } else { - qDebug() << "Sending DS check in. There are" << NodeList::getInstance()->getNumNoReplyDomainCheckIns() << "unreplied."; NodeList::getInstance()->sendDomainServerCheckIn(); } }