From 82b68fce8d61627a223c7a79359b64c9f756dff1 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 15 Mar 2018 18:32:48 -0700 Subject: [PATCH 01/48] Quick attempt at using openssl HMAC in NLPacket --- libraries/networking/src/NLPacket.cpp | 12 ++++++ libraries/shared/src/HmacAuth.cpp | 55 +++++++++++++++++++++++++++ libraries/shared/src/HmacAuth.h | 32 ++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 libraries/shared/src/HmacAuth.cpp create mode 100644 libraries/shared/src/HmacAuth.h diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 5c5077691b..9df84e6abc 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -11,6 +11,8 @@ #include "NLPacket.h" +#include "HmacAuth.h" + int NLPacket::localHeaderSize(PacketType type) { bool nonSourced = PacketTypeEnum::getNonSourcedPackets().contains(type); bool nonVerified = PacketTypeEnum::getNonVerifiedPackets().contains(type); @@ -150,6 +152,14 @@ QByteArray NLPacket::verificationHashInHeader(const udt::Packet& packet) { } QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, const QUuid& connectionSecret) { + HmacAuth hash; + int offset = Packet::totalHeaderSize(packet.isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) + + NUM_BYTES_RFC4122_UUID + NUM_BYTES_MD5_HASH; + hash.setKey(connectionSecret); + hash.addData(packet.getData() + offset, packet.getDataSize() - offset); + auto hashResult(hash.result()); + return QByteArray((const char*) hashResult.data(), (int) hashResult.size()); + /* QCryptographicHash hash(QCryptographicHash::Md5); int offset = Packet::totalHeaderSize(packet.isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) @@ -161,6 +171,8 @@ QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, const QUu // return the hash return hash.result(); + */ + } void NLPacket::writeTypeAndVersion() { diff --git a/libraries/shared/src/HmacAuth.cpp b/libraries/shared/src/HmacAuth.cpp new file mode 100644 index 0000000000..6cc6835329 --- /dev/null +++ b/libraries/shared/src/HmacAuth.cpp @@ -0,0 +1,55 @@ +// +// HmacAuth.cpp + +#include + +#include "HmacAuth.h" + +#include + +HmacAuth::HmacAuth(AuthMethod authMethod) + : _hmacContext(new(HMAC_CTX)) + , _authMethod(authMethod) { + HMAC_CTX_init(_hmacContext.get()); +} + +HmacAuth::~HmacAuth() { + HMAC_CTX_cleanup(_hmacContext.get()); +} + +bool HmacAuth::setKey(const char * keyValue, int keyLen) { + const EVP_MD * sslStruct = nullptr; + + switch (_authMethod) + { + case SHA1: + sslStruct = EVP_sha1(); + break; + + case RIPEMD160: + sslStruct = EVP_ripemd160(); + break; + + default: + return false; + } + + return (bool) HMAC_Init(_hmacContext.get(), keyValue, keyLen, sslStruct); +} + +bool HmacAuth::setKey(const QUuid& uidKey) { + const QByteArray rfcBytes(uidKey.toRfc4122()); + return setKey(rfcBytes.constData(), rfcBytes.length()); +} + +bool HmacAuth::addData(const char * data, int dataLen) { + return (bool) HMAC_Update(_hmacContext.get(), reinterpret_cast(data), dataLen); +} + +HmacAuth::HmacHash HmacAuth::result() { + HmacHash hashValue(EVP_MAX_MD_SIZE); + unsigned int hashLen; + HMAC_Final(_hmacContext.get(), &hashValue[0], &hashLen); + hashValue.resize((size_t) hashLen); + return hashValue; +} diff --git a/libraries/shared/src/HmacAuth.h b/libraries/shared/src/HmacAuth.h new file mode 100644 index 0000000000..9d90f5fb4d --- /dev/null +++ b/libraries/shared/src/HmacAuth.h @@ -0,0 +1,32 @@ +// +// HmacAuth.h +// libraries/shared/src + +#ifndef hifi_HmacAuth_h +#define hifi_HmacAuth_h + +#include +#include + +struct hmac_ctx_st; +class QUuid; + +class HmacAuth { +public: + enum AuthMethod { SHA1, RIPEMD160 }; + typedef std::vector HmacHash; + + HmacAuth(AuthMethod authMethod = SHA1); + ~HmacAuth(); + + bool setKey(const char * keyValue, int keyLen); + bool setKey(const QUuid& uidKey); + bool addData(const char * data, int dataLen); + HmacHash result(); + +private: + std::unique_ptr _hmacContext; + AuthMethod _authMethod { SHA1 }; +}; + +#endif // hifi_HmacAuth_h From 480f76c21aaa92f358691fa9fc296bd65f308523 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 16 Mar 2018 11:50:03 -0700 Subject: [PATCH 02/48] Quick trial of HMAC-MD5 auth + timings --- libraries/networking/src/NLPacket.cpp | 18 ++++++++++++++++++ libraries/shared/src/HmacAuth.cpp | 12 ++++++++++++ libraries/shared/src/HmacAuth.h | 6 +++--- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 9df84e6abc..988e86afc2 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -13,6 +13,12 @@ #include "HmacAuth.h" +#define HIFI_HASH_TIMINGS +#ifdef HIFI_HASH_TIMINGS +#include "NetworkLogging.h" +#include "SharedUtil.h" +#endif + int NLPacket::localHeaderSize(PacketType type) { bool nonSourced = PacketTypeEnum::getNonSourcedPackets().contains(type); bool nonVerified = PacketTypeEnum::getNonVerifiedPackets().contains(type); @@ -230,7 +236,19 @@ void NLPacket::writeVerificationHashGivenSecret(const QUuid& connectionSecret) c auto offset = Packet::totalHeaderSize(isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID; +#ifdef HIFI_HASH_TIMINGS + static quint64 totalTime = 0; + static int timedHashes = 0; + quint64 startTime = usecTimestampNow(); +#endif QByteArray verificationHash = hashForPacketAndSecret(*this, connectionSecret); +#ifdef HIFI_HASH_TIMINGS + quint64 endTime = usecTimestampNow(); + totalTime += endTime - startTime; + if ((++timedHashes % 20) == 0) { + qCDebug(networking) << "Average packet hash time " << (totalTime / timedHashes / 1000.0f) << " ms"; + } +#endif memcpy(_packet.get() + offset, verificationHash.data(), verificationHash.size()); } diff --git a/libraries/shared/src/HmacAuth.cpp b/libraries/shared/src/HmacAuth.cpp index 6cc6835329..469d77c624 100644 --- a/libraries/shared/src/HmacAuth.cpp +++ b/libraries/shared/src/HmacAuth.cpp @@ -22,10 +22,22 @@ bool HmacAuth::setKey(const char * keyValue, int keyLen) { switch (_authMethod) { + case MD5: + sslStruct = EVP_md5(); + break; + case SHA1: sslStruct = EVP_sha1(); break; + case SHA224: + sslStruct = EVP_sha224(); + break; + + case SHA256: + sslStruct = EVP_sha256(); + break; + case RIPEMD160: sslStruct = EVP_ripemd160(); break; diff --git a/libraries/shared/src/HmacAuth.h b/libraries/shared/src/HmacAuth.h index 9d90f5fb4d..1ed6be0eb0 100644 --- a/libraries/shared/src/HmacAuth.h +++ b/libraries/shared/src/HmacAuth.h @@ -13,10 +13,10 @@ class QUuid; class HmacAuth { public: - enum AuthMethod { SHA1, RIPEMD160 }; + enum AuthMethod { MD5, SHA1, SHA224, SHA256, RIPEMD160 }; typedef std::vector HmacHash; - HmacAuth(AuthMethod authMethod = SHA1); + explicit HmacAuth(AuthMethod authMethod = MD5); ~HmacAuth(); bool setKey(const char * keyValue, int keyLen); @@ -26,7 +26,7 @@ public: private: std::unique_ptr _hmacContext; - AuthMethod _authMethod { SHA1 }; + AuthMethod _authMethod { MD5 }; }; #endif // hifi_HmacAuth_h From db8a1ccb3e16a8f561d1eae4daa29f12d0b2878a Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 16 Mar 2018 18:03:13 -0700 Subject: [PATCH 03/48] Set HMAC key once and reuse OpenSSL context Store the HMAC wrapper in Node. Unfortunately requires a lot of plumbing down to NLPacket. Added a mutex to the wrapper since suspicious crashes occurred. Authentication times seem to be comparable to existing MD5. --- libraries/networking/src/LimitedNodeList.cpp | 38 +++++++++++--------- libraries/networking/src/LimitedNodeList.h | 8 ++--- libraries/networking/src/NLPacket.cpp | 15 ++++---- libraries/networking/src/NLPacket.h | 6 ++-- libraries/networking/src/Node.cpp | 7 +++- libraries/networking/src/Node.h | 7 +++- libraries/shared/src/HmacAuth.cpp | 5 +++ libraries/shared/src/HmacAuth.h | 2 ++ 8 files changed, 56 insertions(+), 32 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 0803e380f2..861629fd72 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -36,6 +36,7 @@ #include "HifiSockAddr.h" #include "NetworkLogging.h" #include "udt/Packet.h" +#include "HmacAuth.h" static Setting::Handle LIMITED_NODELIST_LOCAL_PORT("LimitedNodeList.LocalPort", 0); @@ -319,7 +320,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe if (verifiedPacket && !ignoreVerification) { QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet); - QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getConnectionSecret()); + QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getConnectionSecret(), sourceNode->getAuthenticateHash()); // check if the md5 hash in the header matches the hash we would expect if (packetHeaderHash != expectedHash) { @@ -363,7 +364,7 @@ void LimitedNodeList::collectPacketStats(const NLPacket& packet) { _numCollectedBytes += packet.getDataSize(); } -void LimitedNodeList::fillPacketHeader(const NLPacket& packet, const QUuid& connectionSecret) { +void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HmacAuth& hmacAuth, const QUuid& connectionSecret) { if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) { packet.writeSourceID(getSessionUUID()); } @@ -371,7 +372,7 @@ void LimitedNodeList::fillPacketHeader(const NLPacket& packet, const QUuid& conn if (!connectionSecret.isNull() && !PacketTypeEnum::getNonSourcedPackets().contains(packet.getType()) && !PacketTypeEnum::getNonVerifiedPackets().contains(packet.getType())) { - packet.writeVerificationHashGivenSecret(connectionSecret); + packet.writeVerificationHashGivenSecret(hmacAuth, connectionSecret); } } @@ -387,17 +388,18 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const Node& emit dataSent(destinationNode.getType(), packet.getDataSize()); destinationNode.recordBytesSent(packet.getDataSize()); - return sendUnreliablePacket(packet, *destinationNode.getActiveSocket(), destinationNode.getConnectionSecret()); + return sendUnreliablePacket(packet, *destinationNode.getActiveSocket(), destinationNode.getAuthenticateHash(), + destinationNode.getConnectionSecret()); } qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, - const QUuid& connectionSecret) { + HmacAuth& hmacAuth, const QUuid& connectionSecret) { Q_ASSERT(!packet.isPartOfMessage()); Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket", "Trying to send a reliable packet unreliably."); collectPacketStats(packet); - fillPacketHeader(packet, connectionSecret); + fillPacketHeader(packet, hmacAuth, connectionSecret); return _nodeSocket.writePacket(packet, sockAddr); } @@ -410,7 +412,8 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& emit dataSent(destinationNode.getType(), packet->getDataSize()); destinationNode.recordBytesSent(packet->getDataSize()); - return sendPacket(std::move(packet), *activeSocket, destinationNode.getConnectionSecret()); + return sendPacket(std::move(packet), *activeSocket, destinationNode.getAuthenticateHash(), + destinationNode.getConnectionSecret()); } else { qCDebug(networking) << "LimitedNodeList::sendPacket called without active socket for node" << destinationNode << "- not sending"; return ERROR_SENDING_PACKET_BYTES; @@ -418,18 +421,18 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& } qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const HifiSockAddr& sockAddr, - const QUuid& connectionSecret) { + HmacAuth& hmacAuth, const QUuid& connectionSecret) { Q_ASSERT(!packet->isPartOfMessage()); if (packet->isReliable()) { collectPacketStats(*packet); - fillPacketHeader(*packet, connectionSecret); + fillPacketHeader(*packet, hmacAuth, connectionSecret); auto size = packet->getDataSize(); _nodeSocket.writePacket(std::move(packet), sockAddr); return size; } else { - return sendUnreliablePacket(*packet, sockAddr, connectionSecret); + return sendUnreliablePacket(*packet, sockAddr, hmacAuth, connectionSecret); } } @@ -444,7 +447,8 @@ qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetLi packetList.closeCurrentPacket(); while (!packetList._packets.empty()) { - bytesSent += sendPacket(packetList.takeFront(), *activeSocket, connectionSecret); + bytesSent += sendPacket(packetList.takeFront(), *activeSocket, + destinationNode.getAuthenticateHash(), connectionSecret); } emit dataSent(destinationNode.getType(), bytesSent); @@ -457,14 +461,14 @@ qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetLi } qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, - const QUuid& connectionSecret) { + HmacAuth& hmacAuth, const QUuid& connectionSecret) { qint64 bytesSent = 0; // close the last packet in the list packetList.closeCurrentPacket(); while (!packetList._packets.empty()) { - bytesSent += sendPacket(packetList.takeFront(), sockAddr, connectionSecret); + bytesSent += sendPacket(packetList.takeFront(), sockAddr, hmacAuth, connectionSecret); } return bytesSent; @@ -474,10 +478,11 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, // close the last packet in the list packetList->closeCurrentPacket(); + HmacAuth unusedHmac; for (std::unique_ptr& packet : packetList->_packets) { NLPacket* nlPacket = static_cast(packet.get()); collectPacketStats(*nlPacket); - fillPacketHeader(*nlPacket); + fillPacketHeader(*nlPacket, unusedHmac); } return _nodeSocket.writePacketList(std::move(packetList), sockAddr); @@ -492,7 +497,7 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, for (std::unique_ptr& packet : packetList->_packets) { NLPacket* nlPacket = static_cast(packet.get()); collectPacketStats(*nlPacket); - fillPacketHeader(*nlPacket, destinationNode.getConnectionSecret()); + fillPacketHeader(*nlPacket, destinationNode.getAuthenticateHash(), destinationNode.getConnectionSecret()); } return _nodeSocket.writePacketList(std::move(packetList), *activeSocket); @@ -515,7 +520,8 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& auto& destinationSockAddr = (overridenSockAddr.isNull()) ? *destinationNode.getActiveSocket() : overridenSockAddr; - return sendPacket(std::move(packet), destinationSockAddr, destinationNode.getConnectionSecret()); + return sendPacket(std::move(packet), destinationSockAddr, destinationNode.getAuthenticateHash(), + destinationNode.getConnectionSecret()); } int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointer message, SharedNodePointer sendingNode) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 7165b3dd63..8e73440f5b 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -132,18 +132,18 @@ public: // either to a node (via its active socket) or to a manual sockaddr qint64 sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode); qint64 sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, - const QUuid& connectionSecret = QUuid()); + HmacAuth& hmacAuth = HmacAuth(), const QUuid& connectionSecret = QUuid()); // use sendPacket to send a moved unreliable or reliable NL packet to a node's active socket or manual sockaddr qint64 sendPacket(std::unique_ptr packet, const Node& destinationNode); qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr& sockAddr, - const QUuid& connectionSecret = QUuid()); + HmacAuth& hmacAuth = HmacAuth(), const QUuid& connectionSecret = QUuid()); // use sendUnreliableUnorderedPacketList to unreliably send separate packets from the packet list // either to a node's active socket or to a manual sockaddr qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const Node& destinationNode); qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, - const QUuid& connectionSecret = QUuid()); + HmacAuth& hmacAuth = HmacAuth(), const QUuid& connectionSecret = QUuid()); // use sendPacketList to send reliable packet lists (ordered or unordered) to a node's active socket // or to a manual sock addr @@ -364,7 +364,7 @@ protected: qint64 writePacket(const NLPacket& packet, const HifiSockAddr& destinationSockAddr, const QUuid& connectionSecret = QUuid()); void collectPacketStats(const NLPacket& packet); - void fillPacketHeader(const NLPacket& packet, const QUuid& connectionSecret = QUuid()); + void fillPacketHeader(const NLPacket& packet, HmacAuth& hmacAuth, const QUuid& connectionSecret = QUuid()); void setLocalSocket(const HifiSockAddr& sockAddr); diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 988e86afc2..b32c1f1f7f 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -157,15 +157,15 @@ QByteArray NLPacket::verificationHashInHeader(const udt::Packet& packet) { return QByteArray(packet.getData() + offset, NUM_BYTES_MD5_HASH); } -QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, const QUuid& connectionSecret) { - HmacAuth hash; +QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, const QUuid& connectionSecret, HmacAuth& hash) { +#define HIFI_USE_HMAC +#ifdef HIFI_USE_HMAC int offset = Packet::totalHeaderSize(packet.isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID + NUM_BYTES_MD5_HASH; - hash.setKey(connectionSecret); hash.addData(packet.getData() + offset, packet.getDataSize() - offset); auto hashResult(hash.result()); return QByteArray((const char*) hashResult.data(), (int) hashResult.size()); - /* +#else QCryptographicHash hash(QCryptographicHash::Md5); int offset = Packet::totalHeaderSize(packet.isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) @@ -177,8 +177,7 @@ QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, const QUu // return the hash return hash.result(); - */ - +#endif } void NLPacket::writeTypeAndVersion() { @@ -230,7 +229,7 @@ void NLPacket::writeSourceID(const QUuid& sourceID) const { _sourceID = sourceID; } -void NLPacket::writeVerificationHashGivenSecret(const QUuid& connectionSecret) const { +void NLPacket::writeVerificationHashGivenSecret(HmacAuth& hmacAuth, const QUuid& connectionSecret) const { Q_ASSERT(!PacketTypeEnum::getNonSourcedPackets().contains(_type) && !PacketTypeEnum::getNonVerifiedPackets().contains(_type)); @@ -241,7 +240,7 @@ void NLPacket::writeVerificationHashGivenSecret(const QUuid& connectionSecret) c static int timedHashes = 0; quint64 startTime = usecTimestampNow(); #endif - QByteArray verificationHash = hashForPacketAndSecret(*this, connectionSecret); + QByteArray verificationHash = hashForPacketAndSecret(*this, connectionSecret, hmacAuth); #ifdef HIFI_HASH_TIMINGS quint64 endTime = usecTimestampNow(); totalTime += endTime - startTime; diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index f49cc47645..f38f29ec36 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -18,6 +18,8 @@ #include "udt/Packet.h" +class HmacAuth; + class NLPacket : public udt::Packet { Q_OBJECT public: @@ -71,7 +73,7 @@ public: static QUuid sourceIDInHeader(const udt::Packet& packet); static QByteArray verificationHashInHeader(const udt::Packet& packet); - static QByteArray hashForPacketAndSecret(const udt::Packet& packet, const QUuid& connectionSecret); + static QByteArray hashForPacketAndSecret(const udt::Packet& packet, const QUuid& connectionSecret, HmacAuth& hash); PacketType getType() const { return _type; } void setType(PacketType type); @@ -82,7 +84,7 @@ public: const QUuid& getSourceID() const { return _sourceID; } void writeSourceID(const QUuid& sourceID) const; - void writeVerificationHashGivenSecret(const QUuid& connectionSecret) const; + void writeVerificationHashGivenSecret(HmacAuth& hmacAuth, const QUuid& connectionSecret) const; protected: diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index bd895c8ef1..6669c68a2e 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -86,9 +86,10 @@ NodeType_t NodeType::fromString(QString type) { Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, - const HifiSockAddr& localSocket, QObject* parent) : + const HifiSockAddr& localSocket, QObject* parent) : NetworkPeer(uuid, publicSocket, localSocket, parent), _type(type), + _authenticateHash(new HmacAuth), _pingMs(-1), // "Uninitialized" _clockSkewUsec(0), _mutex(), @@ -192,3 +193,7 @@ QDebug operator<<(QDebug debug, const Node& node) { debug.nospace() << node.getPublicSocket() << "/" << node.getLocalSocket(); return debug.nospace(); } + +void Node::_updateAuthenticateHash() { + _authenticateHash->setKey(_connectionSecret); +} diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 93b6a649d4..80d51202d5 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -33,6 +33,7 @@ #include "SimpleMovingAverage.h" #include "MovingPercentile.h" #include "NodePermissions.h" +#include "HmacAuth.h" class Node : public NetworkPeer { Q_OBJECT @@ -55,7 +56,8 @@ public: void setIsUpstream(bool isUpstream) { _isUpstream = isUpstream; } const QUuid& getConnectionSecret() const { return _connectionSecret; } - void setConnectionSecret(const QUuid& connectionSecret) { _connectionSecret = connectionSecret; } + void setConnectionSecret(const QUuid& connectionSecret) { _connectionSecret = connectionSecret; _updateAuthenticateHash(); } + HmacAuth& getAuthenticateHash() const { return *_authenticateHash; } NodeData* getLinkedData() const { return _linkedData.get(); } void setLinkedData(std::unique_ptr linkedData) { _linkedData = std::move(linkedData); } @@ -94,9 +96,12 @@ private: Node(const Node &otherNode); Node& operator=(Node otherNode); + void _updateAuthenticateHash(); + NodeType_t _type; QUuid _connectionSecret; + std::unique_ptr _authenticateHash; std::unique_ptr _linkedData; bool _isReplicated { false }; int _pingMs; diff --git a/libraries/shared/src/HmacAuth.cpp b/libraries/shared/src/HmacAuth.cpp index 469d77c624..ca0ec39b94 100644 --- a/libraries/shared/src/HmacAuth.cpp +++ b/libraries/shared/src/HmacAuth.cpp @@ -46,6 +46,7 @@ bool HmacAuth::setKey(const char * keyValue, int keyLen) { return false; } + QMutexLocker lock(&_lock); return (bool) HMAC_Init(_hmacContext.get(), keyValue, keyLen, sslStruct); } @@ -55,13 +56,17 @@ bool HmacAuth::setKey(const QUuid& uidKey) { } bool HmacAuth::addData(const char * data, int dataLen) { + QMutexLocker lock(&_lock); return (bool) HMAC_Update(_hmacContext.get(), reinterpret_cast(data), dataLen); } HmacAuth::HmacHash HmacAuth::result() { HmacHash hashValue(EVP_MAX_MD_SIZE); unsigned int hashLen; + QMutexLocker lock(&_lock); HMAC_Final(_hmacContext.get(), &hashValue[0], &hashLen); hashValue.resize((size_t) hashLen); + // Clear state for possible reuse. + HMAC_Init(_hmacContext.get(), nullptr, 0, nullptr); return hashValue; } diff --git a/libraries/shared/src/HmacAuth.h b/libraries/shared/src/HmacAuth.h index 1ed6be0eb0..305e1a36ed 100644 --- a/libraries/shared/src/HmacAuth.h +++ b/libraries/shared/src/HmacAuth.h @@ -7,6 +7,7 @@ #include #include +#include struct hmac_ctx_st; class QUuid; @@ -25,6 +26,7 @@ public: HmacHash result(); private: + QMutex _lock; std::unique_ptr _hmacContext; AuthMethod _authMethod { MD5 }; }; From d889384d946f8558f261084fc417cb6b2cc958da Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 19 Mar 2018 09:41:57 -0700 Subject: [PATCH 04/48] Use elaborated type-specifier for openssl internal class --- libraries/shared/src/HmacAuth.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/shared/src/HmacAuth.h b/libraries/shared/src/HmacAuth.h index 305e1a36ed..b39423de95 100644 --- a/libraries/shared/src/HmacAuth.h +++ b/libraries/shared/src/HmacAuth.h @@ -9,7 +9,6 @@ #include #include -struct hmac_ctx_st; class QUuid; class HmacAuth { @@ -27,7 +26,7 @@ public: private: QMutex _lock; - std::unique_ptr _hmacContext; + std::unique_ptr _hmacContext; AuthMethod _authMethod { MD5 }; }; From af21cac0c2a3587c2b3d6f12613d2b84ef6f0aa7 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 19 Mar 2018 12:53:16 -0700 Subject: [PATCH 05/48] Fixes for gcc --- libraries/networking/src/LimitedNodeList.h | 6 +++--- libraries/shared/src/HmacAuth.cpp | 2 ++ libraries/shared/src/HmacAuth.h | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 8e73440f5b..638f3efefc 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -132,18 +132,18 @@ public: // either to a node (via its active socket) or to a manual sockaddr qint64 sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode); qint64 sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth = HmacAuth(), const QUuid& connectionSecret = QUuid()); + HmacAuth& hmacAuth = HmacAuth::nullHmacAuth, const QUuid& connectionSecret = QUuid()); // use sendPacket to send a moved unreliable or reliable NL packet to a node's active socket or manual sockaddr qint64 sendPacket(std::unique_ptr packet, const Node& destinationNode); qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth = HmacAuth(), const QUuid& connectionSecret = QUuid()); + HmacAuth& hmacAuth = HmacAuth::nullHmacAuth, const QUuid& connectionSecret = QUuid()); // use sendUnreliableUnorderedPacketList to unreliably send separate packets from the packet list // either to a node's active socket or to a manual sockaddr qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const Node& destinationNode); qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth = HmacAuth(), const QUuid& connectionSecret = QUuid()); + HmacAuth& hmacAuth = HmacAuth::nullHmacAuth, const QUuid& connectionSecret = QUuid()); // use sendPacketList to send reliable packet lists (ordered or unordered) to a node's active socket // or to a manual sock addr diff --git a/libraries/shared/src/HmacAuth.cpp b/libraries/shared/src/HmacAuth.cpp index ca0ec39b94..47f0e4d224 100644 --- a/libraries/shared/src/HmacAuth.cpp +++ b/libraries/shared/src/HmacAuth.cpp @@ -7,6 +7,8 @@ #include +HmacAuth HmacAuth::nullHmacAuth; + HmacAuth::HmacAuth(AuthMethod authMethod) : _hmacContext(new(HMAC_CTX)) , _authMethod(authMethod) { diff --git a/libraries/shared/src/HmacAuth.h b/libraries/shared/src/HmacAuth.h index b39423de95..4970f08ca6 100644 --- a/libraries/shared/src/HmacAuth.h +++ b/libraries/shared/src/HmacAuth.h @@ -24,6 +24,8 @@ public: bool addData(const char * data, int dataLen); HmacHash result(); + static HmacAuth nullHmacAuth; + private: QMutex _lock; std::unique_ptr _hmacContext; From da7298b8bde004947d4a748d723f074b1f95d29b Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 19 Mar 2018 15:28:44 -0700 Subject: [PATCH 06/48] Support only HMAC - take out passing around of secret UUID Also other clean-up for production use. --- libraries/networking/src/LimitedNodeList.cpp | 38 +++++++++----------- libraries/networking/src/LimitedNodeList.h | 13 +++---- libraries/networking/src/NLPacket.cpp | 21 ++--------- libraries/networking/src/NLPacket.h | 4 +-- libraries/shared/src/HmacAuth.cpp | 10 ++++-- libraries/shared/src/HmacAuth.h | 11 ++++-- 6 files changed, 43 insertions(+), 54 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 861629fd72..d09e379909 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -320,7 +320,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe if (verifiedPacket && !ignoreVerification) { QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet); - QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getConnectionSecret(), sourceNode->getAuthenticateHash()); + QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getAuthenticateHash()); // check if the md5 hash in the header matches the hash we would expect if (packetHeaderHash != expectedHash) { @@ -364,15 +364,15 @@ void LimitedNodeList::collectPacketStats(const NLPacket& packet) { _numCollectedBytes += packet.getDataSize(); } -void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HmacAuth& hmacAuth, const QUuid& connectionSecret) { +void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HmacAuth * hmacAuth) { if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) { packet.writeSourceID(getSessionUUID()); } - if (!connectionSecret.isNull() + if (hmacAuth && !PacketTypeEnum::getNonSourcedPackets().contains(packet.getType()) && !PacketTypeEnum::getNonVerifiedPackets().contains(packet.getType())) { - packet.writeVerificationHashGivenSecret(hmacAuth, connectionSecret); + packet.writeVerificationHashGivenSecret(*hmacAuth); } } @@ -388,18 +388,17 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const Node& emit dataSent(destinationNode.getType(), packet.getDataSize()); destinationNode.recordBytesSent(packet.getDataSize()); - return sendUnreliablePacket(packet, *destinationNode.getActiveSocket(), destinationNode.getAuthenticateHash(), - destinationNode.getConnectionSecret()); + return sendUnreliablePacket(packet, *destinationNode.getActiveSocket(), &destinationNode.getAuthenticateHash()); } qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth, const QUuid& connectionSecret) { + HmacAuth * hmacAuth) { Q_ASSERT(!packet.isPartOfMessage()); Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket", "Trying to send a reliable packet unreliably."); collectPacketStats(packet); - fillPacketHeader(packet, hmacAuth, connectionSecret); + fillPacketHeader(packet, hmacAuth); return _nodeSocket.writePacket(packet, sockAddr); } @@ -412,8 +411,7 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& emit dataSent(destinationNode.getType(), packet->getDataSize()); destinationNode.recordBytesSent(packet->getDataSize()); - return sendPacket(std::move(packet), *activeSocket, destinationNode.getAuthenticateHash(), - destinationNode.getConnectionSecret()); + return sendPacket(std::move(packet), *activeSocket, &destinationNode.getAuthenticateHash()); } else { qCDebug(networking) << "LimitedNodeList::sendPacket called without active socket for node" << destinationNode << "- not sending"; return ERROR_SENDING_PACKET_BYTES; @@ -421,18 +419,18 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& } qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth, const QUuid& connectionSecret) { + HmacAuth * hmacAuth) { Q_ASSERT(!packet->isPartOfMessage()); if (packet->isReliable()) { collectPacketStats(*packet); - fillPacketHeader(*packet, hmacAuth, connectionSecret); + fillPacketHeader(*packet, hmacAuth); auto size = packet->getDataSize(); _nodeSocket.writePacket(std::move(packet), sockAddr); return size; } else { - return sendUnreliablePacket(*packet, sockAddr, hmacAuth, connectionSecret); + return sendUnreliablePacket(*packet, sockAddr, hmacAuth); } } @@ -448,7 +446,7 @@ qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetLi while (!packetList._packets.empty()) { bytesSent += sendPacket(packetList.takeFront(), *activeSocket, - destinationNode.getAuthenticateHash(), connectionSecret); + &destinationNode.getAuthenticateHash()); } emit dataSent(destinationNode.getType(), bytesSent); @@ -461,14 +459,14 @@ qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetLi } qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth, const QUuid& connectionSecret) { + HmacAuth * hmacAuth) { qint64 bytesSent = 0; // close the last packet in the list packetList.closeCurrentPacket(); while (!packetList._packets.empty()) { - bytesSent += sendPacket(packetList.takeFront(), sockAddr, hmacAuth, connectionSecret); + bytesSent += sendPacket(packetList.takeFront(), sockAddr, hmacAuth); } return bytesSent; @@ -478,11 +476,10 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, // close the last packet in the list packetList->closeCurrentPacket(); - HmacAuth unusedHmac; for (std::unique_ptr& packet : packetList->_packets) { NLPacket* nlPacket = static_cast(packet.get()); collectPacketStats(*nlPacket); - fillPacketHeader(*nlPacket, unusedHmac); + fillPacketHeader(*nlPacket, nullptr); } return _nodeSocket.writePacketList(std::move(packetList), sockAddr); @@ -497,7 +494,7 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, for (std::unique_ptr& packet : packetList->_packets) { NLPacket* nlPacket = static_cast(packet.get()); collectPacketStats(*nlPacket); - fillPacketHeader(*nlPacket, destinationNode.getAuthenticateHash(), destinationNode.getConnectionSecret()); + fillPacketHeader(*nlPacket, &destinationNode.getAuthenticateHash()); } return _nodeSocket.writePacketList(std::move(packetList), *activeSocket); @@ -520,8 +517,7 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& auto& destinationSockAddr = (overridenSockAddr.isNull()) ? *destinationNode.getActiveSocket() : overridenSockAddr; - return sendPacket(std::move(packet), destinationSockAddr, destinationNode.getAuthenticateHash(), - destinationNode.getConnectionSecret()); + return sendPacket(std::move(packet), destinationSockAddr, &destinationNode.getAuthenticateHash()); } int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointer message, SharedNodePointer sendingNode) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 638f3efefc..6d546d4d65 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -128,22 +128,19 @@ public: virtual QUuid getDomainUUID() const { assert(false); return QUuid(); } virtual HifiSockAddr getDomainSockAddr() const { assert(false); return HifiSockAddr(); } - // use sendUnreliablePacket to send an unrelaible packet (that you do not need to move) + // use sendUnreliablePacket to send an unreliable packet (that you do not need to move) // either to a node (via its active socket) or to a manual sockaddr qint64 sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode); - qint64 sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth = HmacAuth::nullHmacAuth, const QUuid& connectionSecret = QUuid()); + qint64 sendUnreliablePacket(const NLPacket & packet, const HifiSockAddr & sockAddr, HmacAuth * hmacAuth = nullptr); // use sendPacket to send a moved unreliable or reliable NL packet to a node's active socket or manual sockaddr qint64 sendPacket(std::unique_ptr packet, const Node& destinationNode); - qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth = HmacAuth::nullHmacAuth, const QUuid& connectionSecret = QUuid()); + qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr & sockAddr, HmacAuth * hmacAuth = nullptr); // use sendUnreliableUnorderedPacketList to unreliably send separate packets from the packet list // either to a node's active socket or to a manual sockaddr qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const Node& destinationNode); - qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, - HmacAuth& hmacAuth = HmacAuth::nullHmacAuth, const QUuid& connectionSecret = QUuid()); + qint64 sendUnreliableUnorderedPacketList(NLPacketList & packetList, const HifiSockAddr & sockAddr, HmacAuth * hmacAuth); // use sendPacketList to send reliable packet lists (ordered or unordered) to a node's active socket // or to a manual sock addr @@ -364,7 +361,7 @@ protected: qint64 writePacket(const NLPacket& packet, const HifiSockAddr& destinationSockAddr, const QUuid& connectionSecret = QUuid()); void collectPacketStats(const NLPacket& packet); - void fillPacketHeader(const NLPacket& packet, HmacAuth& hmacAuth, const QUuid& connectionSecret = QUuid()); + void fillPacketHeader(const NLPacket& packet, HmacAuth * hmacAuth); void setLocalSocket(const HifiSockAddr& sockAddr); diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index b32c1f1f7f..99313247e9 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -157,27 +157,12 @@ QByteArray NLPacket::verificationHashInHeader(const udt::Packet& packet) { return QByteArray(packet.getData() + offset, NUM_BYTES_MD5_HASH); } -QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, const QUuid& connectionSecret, HmacAuth& hash) { -#define HIFI_USE_HMAC -#ifdef HIFI_USE_HMAC +QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, HmacAuth& hash) { int offset = Packet::totalHeaderSize(packet.isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID + NUM_BYTES_MD5_HASH; hash.addData(packet.getData() + offset, packet.getDataSize() - offset); auto hashResult(hash.result()); return QByteArray((const char*) hashResult.data(), (int) hashResult.size()); -#else - QCryptographicHash hash(QCryptographicHash::Md5); - - int offset = Packet::totalHeaderSize(packet.isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) - + NUM_BYTES_RFC4122_UUID + NUM_BYTES_MD5_HASH; - - // add the packet payload and the connection UUID - hash.addData(packet.getData() + offset, packet.getDataSize() - offset); - hash.addData(connectionSecret.toRfc4122()); - - // return the hash - return hash.result(); -#endif } void NLPacket::writeTypeAndVersion() { @@ -229,7 +214,7 @@ void NLPacket::writeSourceID(const QUuid& sourceID) const { _sourceID = sourceID; } -void NLPacket::writeVerificationHashGivenSecret(HmacAuth& hmacAuth, const QUuid& connectionSecret) const { +void NLPacket::writeVerificationHashGivenSecret(HmacAuth& hmacAuth) const { Q_ASSERT(!PacketTypeEnum::getNonSourcedPackets().contains(_type) && !PacketTypeEnum::getNonVerifiedPackets().contains(_type)); @@ -240,7 +225,7 @@ void NLPacket::writeVerificationHashGivenSecret(HmacAuth& hmacAuth, const QUuid& static int timedHashes = 0; quint64 startTime = usecTimestampNow(); #endif - QByteArray verificationHash = hashForPacketAndSecret(*this, connectionSecret, hmacAuth); + QByteArray verificationHash = hashForPacketAndSecret(*this, hmacAuth); #ifdef HIFI_HASH_TIMINGS quint64 endTime = usecTimestampNow(); totalTime += endTime - startTime; diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index f38f29ec36..8f73475530 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -73,7 +73,7 @@ public: static QUuid sourceIDInHeader(const udt::Packet& packet); static QByteArray verificationHashInHeader(const udt::Packet& packet); - static QByteArray hashForPacketAndSecret(const udt::Packet& packet, const QUuid& connectionSecret, HmacAuth& hash); + static QByteArray hashForPacketAndSecret(const udt::Packet & packet, HmacAuth & hash); PacketType getType() const { return _type; } void setType(PacketType type); @@ -84,7 +84,7 @@ public: const QUuid& getSourceID() const { return _sourceID; } void writeSourceID(const QUuid& sourceID) const; - void writeVerificationHashGivenSecret(HmacAuth& hmacAuth, const QUuid& connectionSecret) const; + void writeVerificationHashGivenSecret(HmacAuth& hmacAuth) const; protected: diff --git a/libraries/shared/src/HmacAuth.cpp b/libraries/shared/src/HmacAuth.cpp index 47f0e4d224..5d04bb96a4 100644 --- a/libraries/shared/src/HmacAuth.cpp +++ b/libraries/shared/src/HmacAuth.cpp @@ -1,5 +1,13 @@ // // HmacAuth.cpp +// libraries/shared/src +// +// Created by Simon Walton on 3/19/2018. +// Copyright 2018 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 +// #include @@ -7,8 +15,6 @@ #include -HmacAuth HmacAuth::nullHmacAuth; - HmacAuth::HmacAuth(AuthMethod authMethod) : _hmacContext(new(HMAC_CTX)) , _authMethod(authMethod) { diff --git a/libraries/shared/src/HmacAuth.h b/libraries/shared/src/HmacAuth.h index 4970f08ca6..dfc79e8e47 100644 --- a/libraries/shared/src/HmacAuth.h +++ b/libraries/shared/src/HmacAuth.h @@ -1,6 +1,13 @@ // // HmacAuth.h // libraries/shared/src +// +// Created by Simon Walton on 3/19/2018. +// Copyright 2018 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 +// #ifndef hifi_HmacAuth_h #define hifi_HmacAuth_h @@ -14,7 +21,7 @@ class QUuid; class HmacAuth { public: enum AuthMethod { MD5, SHA1, SHA224, SHA256, RIPEMD160 }; - typedef std::vector HmacHash; + using HmacHash = std::vector; explicit HmacAuth(AuthMethod authMethod = MD5); ~HmacAuth(); @@ -24,8 +31,6 @@ public: bool addData(const char * data, int dataLen); HmacHash result(); - static HmacAuth nullHmacAuth; - private: QMutex _lock; std::unique_ptr _hmacContext; From 020a6a65852c7d56d4520199388895e9aa288248 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 19 Mar 2018 15:57:32 -0700 Subject: [PATCH 07/48] Take out hash timing code --- libraries/networking/src/NLPacket.cpp | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 99313247e9..37bb465ca9 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -13,12 +13,6 @@ #include "HmacAuth.h" -#define HIFI_HASH_TIMINGS -#ifdef HIFI_HASH_TIMINGS -#include "NetworkLogging.h" -#include "SharedUtil.h" -#endif - int NLPacket::localHeaderSize(PacketType type) { bool nonSourced = PacketTypeEnum::getNonSourcedPackets().contains(type); bool nonVerified = PacketTypeEnum::getNonVerifiedPackets().contains(type); @@ -220,19 +214,8 @@ void NLPacket::writeVerificationHashGivenSecret(HmacAuth& hmacAuth) const { auto offset = Packet::totalHeaderSize(isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID; -#ifdef HIFI_HASH_TIMINGS - static quint64 totalTime = 0; - static int timedHashes = 0; - quint64 startTime = usecTimestampNow(); -#endif + QByteArray verificationHash = hashForPacketAndSecret(*this, hmacAuth); -#ifdef HIFI_HASH_TIMINGS - quint64 endTime = usecTimestampNow(); - totalTime += endTime - startTime; - if ((++timedHashes % 20) == 0) { - qCDebug(networking) << "Average packet hash time " << (totalTime / timedHashes / 1000.0f) << " ms"; - } -#endif memcpy(_packet.get() + offset, verificationHash.data(), verificationHash.size()); } From 2a486a4c1419f974c1a11b55fdb266d8c42e84c1 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 19 Mar 2018 17:48:11 -0700 Subject: [PATCH 08/48] Bump packet version numbers --- libraries/networking/src/LimitedNodeList.h | 3 ++- .../networking/src/udt/PacketHeaders.cpp | 21 ++++++++-------- libraries/networking/src/udt/PacketHeaders.h | 25 +++++++++++++------ 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 6d546d4d65..612a6ce947 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -140,7 +140,8 @@ public: // use sendUnreliableUnorderedPacketList to unreliably send separate packets from the packet list // either to a node's active socket or to a manual sockaddr qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const Node& destinationNode); - qint64 sendUnreliableUnorderedPacketList(NLPacketList & packetList, const HifiSockAddr & sockAddr, HmacAuth * hmacAuth); + qint64 sendUnreliableUnorderedPacketList(NLPacketList & packetList, const HifiSockAddr & sockAddr, + HmacAuth * hmacAuth = nullptr); // use sendPacketList to send reliable packet lists (ordered or unordered) to a node's active socket // or to a manual sock addr diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index a83924ee58..d6b59d59f9 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -25,30 +25,29 @@ int packetTypeMetaTypeId = qRegisterMetaType(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return static_cast(DomainListVersion::GetMachineFingerprintFromUUIDSupport); + return static_cast(DomainListVersion::UseHmacAuthentication); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::ShadowControl); + return static_cast(EntityVersion::UseHmacAuthentication); case PacketType::EntityQuery: - return static_cast(EntityQueryPacketVersion::RemovedJurisdictions); + return static_cast(EntityQueryPacketVersion::UseHmacAuthentication); case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::FBXReaderNodeReparenting); + return static_cast(AvatarMixerPacketVersion::UseHmacAuthentication); case PacketType::MessagesData: - return static_cast(MessageDataVersion::TextOrBinaryData); + return static_cast(MessageDataVersion::UseHmacAuthentication); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetMappingOperation: case PacketType::AssetMappingOperationReply: - return static_cast(AssetServerPacketVersion::RedirectedMappings); case PacketType::AssetGetInfo: case PacketType::AssetGet: case PacketType::AssetUpload: - return static_cast(AssetServerPacketVersion::RangeRequestSupport); + return static_cast(AssetServerPacketVersion::UseHmacAuthentication); case PacketType::NodeIgnoreRequest: return 18; // Introduction of node ignore request (which replaced an unused packet tpye) @@ -59,10 +58,10 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectRequestVersion::AlwaysHasMachineFingerprint); case PacketType::DomainServerAddedNode: - return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); + return static_cast(DomainServerAddedNodeVersion::UseHmacAuthentication); case PacketType::EntityScriptCallMethod: - return static_cast(EntityScriptCallMethodVersion::ClientCallable); + return static_cast(EntityScriptCallMethodVersion::UseHmacAuthentication); case PacketType::MixedAudio: case PacketType::SilentAudioFrame: @@ -70,13 +69,13 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::MicrophoneAudioNoEcho: case PacketType::MicrophoneAudioWithEcho: case PacketType::AudioStreamStats: - return static_cast(AudioVersion::HighDynamicRangeVolume); + return static_cast(AudioVersion::UseHmacAuthentication); case PacketType::ICEPing: return static_cast(IcePingVersion::SendICEPeerID); case PacketType::DomainSettings: return 18; // replace min_avatar_scale and max_avatar_scale with min_avatar_height and max_avatar_height default: - return 17; + return 18; } } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 98a9087d37..9dc3f2befd 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -231,25 +231,29 @@ enum class EntityVersion : PacketVersion { ZoneStageRemoved, SoftEntities, MaterialEntities, - ShadowControl + ShadowControl, + UseHmacAuthentication }; enum class EntityScriptCallMethodVersion : PacketVersion { ServerCallable = 18, - ClientCallable = 19 + ClientCallable = 19, + UseHmacAuthentication = 20 }; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, JSONFilterWithFamilyTree = 19, ConnectionIdentifier = 20, - RemovedJurisdictions = 21 + RemovedJurisdictions = 21, + UseHmacAuthentication = 22 }; enum class AssetServerPacketVersion: PacketVersion { VegasCongestionControl = 19, RangeRequestSupport, - RedirectedMappings + RedirectedMappings, + UseHmacAuthentication }; enum class AvatarMixerPacketVersion : PacketVersion { @@ -274,7 +278,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { AvatarIdentityLookAtSnapping, UpdatedMannequinDefaultAvatar, AvatarJointDefaultPoseFlags, - FBXReaderNodeReparenting + FBXReaderNodeReparenting, + UseHmacAuthentication }; enum class DomainConnectRequestVersion : PacketVersion { @@ -294,14 +299,16 @@ enum class DomainConnectionDeniedVersion : PacketVersion { enum class DomainServerAddedNodeVersion : PacketVersion { PrePermissionsGrid = 17, - PermissionsGrid + PermissionsGrid, + UseHmacAuthentication }; enum class DomainListVersion : PacketVersion { PrePermissionsGrid = 18, PermissionsGrid, GetUsernameFromUUIDSupport, - GetMachineFingerprintFromUUIDSupport + GetMachineFingerprintFromUUIDSupport, + UseHmacAuthentication }; enum class AudioVersion : PacketVersion { @@ -312,10 +319,12 @@ enum class AudioVersion : PacketVersion { SpaceBubbleChanges, HasPersonalMute, HighDynamicRangeVolume, + UseHmacAuthentication, }; enum class MessageDataVersion : PacketVersion { - TextOrBinaryData = 18 + TextOrBinaryData = 18, + UseHmacAuthentication }; enum class IcePingVersion : PacketVersion { From 8ce03d65b76493ddc4c8d8a92bd3eea6abfc938a Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 20 Mar 2018 18:16:45 -0700 Subject: [PATCH 09/48] Only update the connection secret UUID if it changes Rekeying the openssl HMAC context occasionally causes hash generation errors. It is not clear why. The Node secret never seems to change to check for this before rekeying. Also other clean-up for PR. --- libraries/networking/src/LimitedNodeList.cpp | 4 ++-- libraries/networking/src/NLPacket.h | 2 +- libraries/networking/src/Node.cpp | 8 +++++++- libraries/networking/src/Node.h | 4 +--- libraries/shared/src/HmacAuth.cpp | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d09e379909..7d4ac574da 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -439,14 +439,14 @@ qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetLi if (activeSocket) { qint64 bytesSent = 0; - auto connectionSecret = destinationNode.getConnectionSecret(); + auto& connectionHash = destinationNode.getAuthenticateHash(); // close the last packet in the list packetList.closeCurrentPacket(); while (!packetList._packets.empty()) { bytesSent += sendPacket(packetList.takeFront(), *activeSocket, - &destinationNode.getAuthenticateHash()); + &connectionHash); } emit dataSent(destinationNode.getType(), bytesSent); diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index 8f73475530..88b856cfda 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -73,7 +73,7 @@ public: static QUuid sourceIDInHeader(const udt::Packet& packet); static QByteArray verificationHashInHeader(const udt::Packet& packet); - static QByteArray hashForPacketAndSecret(const udt::Packet & packet, HmacAuth & hash); + static QByteArray hashForPacketAndSecret(const udt::Packet& packet, HmacAuth& hash); PacketType getType() const { return _type; } void setType(PacketType type); diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 6669c68a2e..5af5172580 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -109,6 +109,7 @@ void Node::setType(char type) { _symmetricSocket.setObjectName(typeString); } + void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewMovingPercentile.updatePercentile(clockSkewSample); _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); @@ -194,6 +195,11 @@ QDebug operator<<(QDebug debug, const Node& node) { return debug.nospace(); } -void Node::_updateAuthenticateHash() { +void Node::setConnectionSecret(const QUuid & connectionSecret) { + if (_connectionSecret == connectionSecret) { + return; + } + + _connectionSecret = connectionSecret; _authenticateHash->setKey(_connectionSecret); } diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 80d51202d5..fe99e9c1ca 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -56,7 +56,7 @@ public: void setIsUpstream(bool isUpstream) { _isUpstream = isUpstream; } const QUuid& getConnectionSecret() const { return _connectionSecret; } - void setConnectionSecret(const QUuid& connectionSecret) { _connectionSecret = connectionSecret; _updateAuthenticateHash(); } + void setConnectionSecret(const QUuid& connectionSecret); HmacAuth& getAuthenticateHash() const { return *_authenticateHash; } NodeData* getLinkedData() const { return _linkedData.get(); } @@ -96,8 +96,6 @@ private: Node(const Node &otherNode); Node& operator=(Node otherNode); - void _updateAuthenticateHash(); - NodeType_t _type; QUuid _connectionSecret; diff --git a/libraries/shared/src/HmacAuth.cpp b/libraries/shared/src/HmacAuth.cpp index 5d04bb96a4..f3ffec2c05 100644 --- a/libraries/shared/src/HmacAuth.cpp +++ b/libraries/shared/src/HmacAuth.cpp @@ -55,7 +55,7 @@ bool HmacAuth::setKey(const char * keyValue, int keyLen) { } QMutexLocker lock(&_lock); - return (bool) HMAC_Init(_hmacContext.get(), keyValue, keyLen, sslStruct); + return (bool) HMAC_Init_ex(_hmacContext.get(), keyValue, keyLen, sslStruct, nullptr); } bool HmacAuth::setKey(const QUuid& uidKey) { From eb04f77c3dc71990ea6e4fb21a0b156c234dab98 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 14:04:34 -0700 Subject: [PATCH 10/48] HMAC Auth - code modifications requested by reviewer --- libraries/networking/src/LimitedNodeList.cpp | 16 ++++++------- libraries/networking/src/LimitedNodeList.h | 8 +++---- libraries/networking/src/NLPacket.cpp | 10 ++++---- libraries/networking/src/NLPacket.h | 6 ++--- libraries/networking/src/Node.cpp | 5 ++-- libraries/networking/src/Node.h | 6 ++--- .../shared/src/{HmacAuth.cpp => HMACAuth.cpp} | 23 +++++++++---------- .../shared/src/{HmacAuth.h => HMACAuth.h} | 16 ++++++------- 8 files changed, 44 insertions(+), 46 deletions(-) rename libraries/shared/src/{HmacAuth.cpp => HMACAuth.cpp} (78%) rename libraries/shared/src/{HmacAuth.h => HMACAuth.h} (74%) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 7d4ac574da..ab66b7ae92 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -36,7 +36,7 @@ #include "HifiSockAddr.h" #include "NetworkLogging.h" #include "udt/Packet.h" -#include "HmacAuth.h" +#include "HMACAuth.h" static Setting::Handle LIMITED_NODELIST_LOCAL_PORT("LimitedNodeList.LocalPort", 0); @@ -320,7 +320,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe if (verifiedPacket && !ignoreVerification) { QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet); - QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getAuthenticateHash()); + QByteArray expectedHash = NLPacket::hashForPacketAndHMAC(packet, sourceNode->getAuthenticateHash()); // check if the md5 hash in the header matches the hash we would expect if (packetHeaderHash != expectedHash) { @@ -364,7 +364,7 @@ void LimitedNodeList::collectPacketStats(const NLPacket& packet) { _numCollectedBytes += packet.getDataSize(); } -void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HmacAuth * hmacAuth) { +void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth) { if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) { packet.writeSourceID(getSessionUUID()); } @@ -372,7 +372,7 @@ void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HmacAuth * hmacAu if (hmacAuth && !PacketTypeEnum::getNonSourcedPackets().contains(packet.getType()) && !PacketTypeEnum::getNonVerifiedPackets().contains(packet.getType())) { - packet.writeVerificationHashGivenSecret(*hmacAuth); + packet.writeVerificationHash(*hmacAuth); } } @@ -392,7 +392,7 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const Node& } qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, - HmacAuth * hmacAuth) { + HMACAuth * hmacAuth) { Q_ASSERT(!packet.isPartOfMessage()); Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket", "Trying to send a reliable packet unreliably."); @@ -419,7 +419,7 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& } qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const HifiSockAddr& sockAddr, - HmacAuth * hmacAuth) { + HMACAuth* hmacAuth) { Q_ASSERT(!packet->isPartOfMessage()); if (packet->isReliable()) { collectPacketStats(*packet); @@ -459,7 +459,7 @@ qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetLi } qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, - HmacAuth * hmacAuth) { + HMACAuth* hmacAuth) { qint64 bytesSent = 0; // close the last packet in the list @@ -479,7 +479,7 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList, for (std::unique_ptr& packet : packetList->_packets) { NLPacket* nlPacket = static_cast(packet.get()); collectPacketStats(*nlPacket); - fillPacketHeader(*nlPacket, nullptr); + fillPacketHeader(*nlPacket); } return _nodeSocket.writePacketList(std::move(packetList), sockAddr); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 612a6ce947..eb70fbcbdf 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -131,17 +131,17 @@ public: // use sendUnreliablePacket to send an unreliable packet (that you do not need to move) // either to a node (via its active socket) or to a manual sockaddr qint64 sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode); - qint64 sendUnreliablePacket(const NLPacket & packet, const HifiSockAddr & sockAddr, HmacAuth * hmacAuth = nullptr); + qint64 sendUnreliablePacket(const NLPacket & packet, const HifiSockAddr & sockAddr, HMACAuth * hmacAuth = nullptr); // use sendPacket to send a moved unreliable or reliable NL packet to a node's active socket or manual sockaddr qint64 sendPacket(std::unique_ptr packet, const Node& destinationNode); - qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr & sockAddr, HmacAuth * hmacAuth = nullptr); + qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr & sockAddr, HMACAuth * hmacAuth = nullptr); // use sendUnreliableUnorderedPacketList to unreliably send separate packets from the packet list // either to a node's active socket or to a manual sockaddr qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const Node& destinationNode); qint64 sendUnreliableUnorderedPacketList(NLPacketList & packetList, const HifiSockAddr & sockAddr, - HmacAuth * hmacAuth = nullptr); + HMACAuth * hmacAuth = nullptr); // use sendPacketList to send reliable packet lists (ordered or unordered) to a node's active socket // or to a manual sock addr @@ -362,7 +362,7 @@ protected: qint64 writePacket(const NLPacket& packet, const HifiSockAddr& destinationSockAddr, const QUuid& connectionSecret = QUuid()); void collectPacketStats(const NLPacket& packet); - void fillPacketHeader(const NLPacket& packet, HmacAuth * hmacAuth); + void fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth = nullptr); void setLocalSocket(const HifiSockAddr& sockAddr); diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 37bb465ca9..93274843a6 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -11,7 +11,7 @@ #include "NLPacket.h" -#include "HmacAuth.h" +#include "HMACAuth.h" int NLPacket::localHeaderSize(PacketType type) { bool nonSourced = PacketTypeEnum::getNonSourcedPackets().contains(type); @@ -151,11 +151,11 @@ QByteArray NLPacket::verificationHashInHeader(const udt::Packet& packet) { return QByteArray(packet.getData() + offset, NUM_BYTES_MD5_HASH); } -QByteArray NLPacket::hashForPacketAndSecret(const udt::Packet& packet, HmacAuth& hash) { +QByteArray NLPacket::hashForPacketAndHMAC(const udt::Packet& packet, HMACAuth& hash) { int offset = Packet::totalHeaderSize(packet.isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID + NUM_BYTES_MD5_HASH; hash.addData(packet.getData() + offset, packet.getDataSize() - offset); - auto hashResult(hash.result()); + auto hashResult { hash.result() }; return QByteArray((const char*) hashResult.data(), (int) hashResult.size()); } @@ -208,14 +208,14 @@ void NLPacket::writeSourceID(const QUuid& sourceID) const { _sourceID = sourceID; } -void NLPacket::writeVerificationHashGivenSecret(HmacAuth& hmacAuth) const { +void NLPacket::writeVerificationHash(HMACAuth& hmacAuth) const { Q_ASSERT(!PacketTypeEnum::getNonSourcedPackets().contains(_type) && !PacketTypeEnum::getNonVerifiedPackets().contains(_type)); auto offset = Packet::totalHeaderSize(isPartOfMessage()) + sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID; - QByteArray verificationHash = hashForPacketAndSecret(*this, hmacAuth); + QByteArray verificationHash = hashForPacketAndHMAC(*this, hmacAuth); memcpy(_packet.get() + offset, verificationHash.data(), verificationHash.size()); } diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index 88b856cfda..302598f77c 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -18,7 +18,7 @@ #include "udt/Packet.h" -class HmacAuth; +class HMACAuth; class NLPacket : public udt::Packet { Q_OBJECT @@ -73,7 +73,7 @@ public: static QUuid sourceIDInHeader(const udt::Packet& packet); static QByteArray verificationHashInHeader(const udt::Packet& packet); - static QByteArray hashForPacketAndSecret(const udt::Packet& packet, HmacAuth& hash); + static QByteArray hashForPacketAndHMAC(const udt::Packet& packet, HMACAuth& hash); PacketType getType() const { return _type; } void setType(PacketType type); @@ -84,7 +84,7 @@ public: const QUuid& getSourceID() const { return _sourceID; } void writeSourceID(const QUuid& sourceID) const; - void writeVerificationHashGivenSecret(HmacAuth& hmacAuth) const; + void writeVerificationHash(HMACAuth& hmacAuth) const; protected: diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 5af5172580..132d27d311 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -89,8 +89,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, QObject* parent) : NetworkPeer(uuid, publicSocket, localSocket, parent), _type(type), - _authenticateHash(new HmacAuth), - _pingMs(-1), // "Uninitialized" + _authenticateHash(new HMACAuth), _pingMs(-1), // "Uninitialized" _clockSkewUsec(0), _mutex(), _clockSkewMovingPercentile(30, 0.8f) // moving 80th percentile of 30 samples @@ -195,7 +194,7 @@ QDebug operator<<(QDebug debug, const Node& node) { return debug.nospace(); } -void Node::setConnectionSecret(const QUuid & connectionSecret) { +void Node::setConnectionSecret(const QUuid& connectionSecret) { if (_connectionSecret == connectionSecret) { return; } diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index fe99e9c1ca..5b3b559582 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -33,7 +33,7 @@ #include "SimpleMovingAverage.h" #include "MovingPercentile.h" #include "NodePermissions.h" -#include "HmacAuth.h" +#include "HMACAuth.h" class Node : public NetworkPeer { Q_OBJECT @@ -57,7 +57,7 @@ public: const QUuid& getConnectionSecret() const { return _connectionSecret; } void setConnectionSecret(const QUuid& connectionSecret); - HmacAuth& getAuthenticateHash() const { return *_authenticateHash; } + HMACAuth& getAuthenticateHash() const { return *_authenticateHash; } NodeData* getLinkedData() const { return _linkedData.get(); } void setLinkedData(std::unique_ptr linkedData) { _linkedData = std::move(linkedData); } @@ -99,7 +99,7 @@ private: NodeType_t _type; QUuid _connectionSecret; - std::unique_ptr _authenticateHash; + std::unique_ptr _authenticateHash; std::unique_ptr _linkedData; bool _isReplicated { false }; int _pingMs; diff --git a/libraries/shared/src/HmacAuth.cpp b/libraries/shared/src/HMACAuth.cpp similarity index 78% rename from libraries/shared/src/HmacAuth.cpp rename to libraries/shared/src/HMACAuth.cpp index f3ffec2c05..9abce7b954 100644 --- a/libraries/shared/src/HmacAuth.cpp +++ b/libraries/shared/src/HMACAuth.cpp @@ -1,5 +1,5 @@ // -// HmacAuth.cpp +// HMACAuth.cpp // libraries/shared/src // // Created by Simon Walton on 3/19/2018. @@ -11,25 +11,24 @@ #include -#include "HmacAuth.h" +#include "HMACAuth.h" #include -HmacAuth::HmacAuth(AuthMethod authMethod) +HMACAuth::HMACAuth(AuthMethod authMethod) : _hmacContext(new(HMAC_CTX)) , _authMethod(authMethod) { HMAC_CTX_init(_hmacContext.get()); } -HmacAuth::~HmacAuth() { +HMACAuth::~HMACAuth() { HMAC_CTX_cleanup(_hmacContext.get()); } -bool HmacAuth::setKey(const char * keyValue, int keyLen) { - const EVP_MD * sslStruct = nullptr; +bool HMACAuth::setKey(const char * keyValue, int keyLen) { + const EVP_MD* sslStruct = nullptr; - switch (_authMethod) - { + switch (_authMethod) { case MD5: sslStruct = EVP_md5(); break; @@ -58,18 +57,18 @@ bool HmacAuth::setKey(const char * keyValue, int keyLen) { return (bool) HMAC_Init_ex(_hmacContext.get(), keyValue, keyLen, sslStruct, nullptr); } -bool HmacAuth::setKey(const QUuid& uidKey) { +bool HMACAuth::setKey(const QUuid& uidKey) { const QByteArray rfcBytes(uidKey.toRfc4122()); return setKey(rfcBytes.constData(), rfcBytes.length()); } -bool HmacAuth::addData(const char * data, int dataLen) { +bool HMACAuth::addData(const char * data, int dataLen) { QMutexLocker lock(&_lock); return (bool) HMAC_Update(_hmacContext.get(), reinterpret_cast(data), dataLen); } -HmacAuth::HmacHash HmacAuth::result() { - HmacHash hashValue(EVP_MAX_MD_SIZE); +HMACAuth::HMACHash HMACAuth::result() { + HMACHash hashValue(EVP_MAX_MD_SIZE); unsigned int hashLen; QMutexLocker lock(&_lock); HMAC_Final(_hmacContext.get(), &hashValue[0], &hashLen); diff --git a/libraries/shared/src/HmacAuth.h b/libraries/shared/src/HMACAuth.h similarity index 74% rename from libraries/shared/src/HmacAuth.h rename to libraries/shared/src/HMACAuth.h index dfc79e8e47..4bb20a6464 100644 --- a/libraries/shared/src/HmacAuth.h +++ b/libraries/shared/src/HMACAuth.h @@ -1,6 +1,6 @@ // -// HmacAuth.h -// libraries/shared/src +// HMACAuth.h +// libraries/shared/src // // Created by Simon Walton on 3/19/2018. // Copyright 2018 High Fidelity, Inc. @@ -18,23 +18,23 @@ class QUuid; -class HmacAuth { +class HMACAuth { public: enum AuthMethod { MD5, SHA1, SHA224, SHA256, RIPEMD160 }; - using HmacHash = std::vector; + using HMACHash = std::vector; - explicit HmacAuth(AuthMethod authMethod = MD5); - ~HmacAuth(); + explicit HMACAuth(AuthMethod authMethod = MD5); + ~HMACAuth(); bool setKey(const char * keyValue, int keyLen); bool setKey(const QUuid& uidKey); bool addData(const char * data, int dataLen); - HmacHash result(); + HMACHash result(); private: QMutex _lock; std::unique_ptr _hmacContext; - AuthMethod _authMethod { MD5 }; + AuthMethod _authMethod; }; #endif // hifi_HmacAuth_h From adbb2400ab5c01ca2da0a6b47e286f73b63b8eff Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 14:58:14 -0700 Subject: [PATCH 11/48] HMAC Auth - add openssl to cmake file for lib shared --- libraries/shared/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index 713501aa77..bff842fdd8 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME shared) # TODO: there isn't really a good reason to have Script linked here - let's get what is requiring it out (RegisteredMetaTypes.cpp) setup_hifi_library(Gui Network Script) +include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") if (WIN32) target_link_libraries(${TARGET_NAME} Wbemuuid.lib) From fb16e772ba45f84b1b7dbdfdd3024caa13784e62 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 15:25:50 -0700 Subject: [PATCH 12/48] Move HMACAuth class to networking lib Also reverts addition of openssl headers to shared lib. Commit will require rerunning cmake. --- libraries/{shared => networking}/src/HMACAuth.cpp | 0 libraries/{shared => networking}/src/HMACAuth.h | 0 libraries/shared/CMakeLists.txt | 1 - 3 files changed, 1 deletion(-) rename libraries/{shared => networking}/src/HMACAuth.cpp (100%) rename libraries/{shared => networking}/src/HMACAuth.h (100%) diff --git a/libraries/shared/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp similarity index 100% rename from libraries/shared/src/HMACAuth.cpp rename to libraries/networking/src/HMACAuth.cpp diff --git a/libraries/shared/src/HMACAuth.h b/libraries/networking/src/HMACAuth.h similarity index 100% rename from libraries/shared/src/HMACAuth.h rename to libraries/networking/src/HMACAuth.h diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index bff842fdd8..713501aa77 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -2,7 +2,6 @@ set(TARGET_NAME shared) # TODO: there isn't really a good reason to have Script linked here - let's get what is requiring it out (RegisteredMetaTypes.cpp) setup_hifi_library(Gui Network Script) -include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") if (WIN32) target_link_libraries(${TARGET_NAME} Wbemuuid.lib) From 64973aa334f5f6582ea183c5e026f6dd19b5db31 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 17:06:15 -0700 Subject: [PATCH 13/48] OpenSSL HMAC changes for Android Looks like Android uses OpenSSL 1.1.0, which provides an allocator for its HMAC context. --- libraries/networking/src/HMACAuth.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/networking/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp index 9abce7b954..52c43fe574 100644 --- a/libraries/networking/src/HMACAuth.cpp +++ b/libraries/networking/src/HMACAuth.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include "HMACAuth.h" @@ -16,7 +17,11 @@ #include HMACAuth::HMACAuth(AuthMethod authMethod) +#if OPENSSL_VERSION_NUMBER >= 0x10100000 + : _hmacContext(HMAC_CTX_new()) +#else : _hmacContext(new(HMAC_CTX)) +#endif , _authMethod(authMethod) { HMAC_CTX_init(_hmacContext.get()); } From 755d89464fb326e74311cd68ba648cc4a17e0dbe Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 17:15:49 -0700 Subject: [PATCH 14/48] HMAC Auth - reviewer-requested changes --- libraries/networking/src/HMACAuth.cpp | 2 +- libraries/networking/src/HMACAuth.h | 8 ++++---- libraries/networking/src/LimitedNodeList.cpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/networking/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp index 52c43fe574..dc6d790425 100644 --- a/libraries/networking/src/HMACAuth.cpp +++ b/libraries/networking/src/HMACAuth.cpp @@ -30,7 +30,7 @@ HMACAuth::~HMACAuth() { HMAC_CTX_cleanup(_hmacContext.get()); } -bool HMACAuth::setKey(const char * keyValue, int keyLen) { +bool HMACAuth::setKey(const char* keyValue, int keyLen) { const EVP_MD* sslStruct = nullptr; switch (_authMethod) { diff --git a/libraries/networking/src/HMACAuth.h b/libraries/networking/src/HMACAuth.h index 4bb20a6464..57f9dd64b8 100644 --- a/libraries/networking/src/HMACAuth.h +++ b/libraries/networking/src/HMACAuth.h @@ -1,6 +1,6 @@ // // HMACAuth.h -// libraries/shared/src +// libraries/networking/src // // Created by Simon Walton on 3/19/2018. // Copyright 2018 High Fidelity, Inc. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_HmacAuth_h -#define hifi_HmacAuth_h +#ifndef hifi_HMACAuth_h +#define hifi_HMACAuth_h #include #include @@ -37,4 +37,4 @@ private: AuthMethod _authMethod; }; -#endif // hifi_HmacAuth_h +#endif // hifi_HMACAuth_h diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index ab66b7ae92..d2de034d0e 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -392,7 +392,7 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const Node& } qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, - HMACAuth * hmacAuth) { + HMACAuth* hmacAuth) { Q_ASSERT(!packet.isPartOfMessage()); Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket", "Trying to send a reliable packet unreliably."); From ef087702352784848b93bd60e6a2adcc76dd9121 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 17:17:24 -0700 Subject: [PATCH 15/48] Missed reviewer change --- libraries/networking/src/HMACAuth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp index dc6d790425..1096098cdd 100644 --- a/libraries/networking/src/HMACAuth.cpp +++ b/libraries/networking/src/HMACAuth.cpp @@ -67,7 +67,7 @@ bool HMACAuth::setKey(const QUuid& uidKey) { return setKey(rfcBytes.constData(), rfcBytes.length()); } -bool HMACAuth::addData(const char * data, int dataLen) { +bool HMACAuth::addData(const char* data, int dataLen) { QMutexLocker lock(&_lock); return (bool) HMAC_Update(_hmacContext.get(), reinterpret_cast(data), dataLen); } From 3ced1c89237b5706b5365b06f4d2810e8d8aa565 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 17:31:54 -0700 Subject: [PATCH 16/48] More Openssl 1.1 (Android) fixes --- libraries/networking/src/HMACAuth.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp index 1096098cdd..77b4fb67b2 100644 --- a/libraries/networking/src/HMACAuth.cpp +++ b/libraries/networking/src/HMACAuth.cpp @@ -16,12 +16,17 @@ #include -HMACAuth::HMACAuth(AuthMethod authMethod) #if OPENSSL_VERSION_NUMBER >= 0x10100000 +HMACAuth::HMACAuth(AuthMethod authMethod) : _hmacContext(HMAC_CTX_new()) + , _authMethod(authMethod) { } + +HMACAuth::~HMACAuth() { } + #else + +HMACAuth::HMACAuth(AuthMethod authMethod) : _hmacContext(new(HMAC_CTX)) -#endif , _authMethod(authMethod) { HMAC_CTX_init(_hmacContext.get()); } @@ -29,6 +34,7 @@ HMACAuth::HMACAuth(AuthMethod authMethod) HMACAuth::~HMACAuth() { HMAC_CTX_cleanup(_hmacContext.get()); } +#endif bool HMACAuth::setKey(const char* keyValue, int keyLen) { const EVP_MD* sslStruct = nullptr; @@ -79,6 +85,6 @@ HMACAuth::HMACHash HMACAuth::result() { HMAC_Final(_hmacContext.get(), &hashValue[0], &hashLen); hashValue.resize((size_t) hashLen); // Clear state for possible reuse. - HMAC_Init(_hmacContext.get(), nullptr, 0, nullptr); + HMAC_Init_ex(_hmacContext.get(), nullptr, 0, nullptr, nullptr); return hashValue; } From d58b2acc8ce5c7ee8fd3211d41e8937656052e79 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 18:25:26 -0700 Subject: [PATCH 17/48] Use raw pointer for possibly-opaque openssl context type --- libraries/networking/src/HMACAuth.cpp | 18 +++++++++++------- libraries/networking/src/HMACAuth.h | 6 +++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/libraries/networking/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp index 77b4fb67b2..baeffeadeb 100644 --- a/libraries/networking/src/HMACAuth.cpp +++ b/libraries/networking/src/HMACAuth.cpp @@ -21,18 +21,22 @@ HMACAuth::HMACAuth(AuthMethod authMethod) : _hmacContext(HMAC_CTX_new()) , _authMethod(authMethod) { } -HMACAuth::~HMACAuth() { } +HMACAuth::~HMACAuth() +{ + HMAC_CTX_free(_hmacContext); +} #else HMACAuth::HMACAuth(AuthMethod authMethod) : _hmacContext(new(HMAC_CTX)) , _authMethod(authMethod) { - HMAC_CTX_init(_hmacContext.get()); + HMAC_CTX_init(_hmacContext); } HMACAuth::~HMACAuth() { - HMAC_CTX_cleanup(_hmacContext.get()); + HMAC_CTX_cleanup(_hmacContext); + delete _hmacContext; } #endif @@ -65,7 +69,7 @@ bool HMACAuth::setKey(const char* keyValue, int keyLen) { } QMutexLocker lock(&_lock); - return (bool) HMAC_Init_ex(_hmacContext.get(), keyValue, keyLen, sslStruct, nullptr); + return (bool) HMAC_Init_ex(_hmacContext, keyValue, keyLen, sslStruct, nullptr); } bool HMACAuth::setKey(const QUuid& uidKey) { @@ -75,16 +79,16 @@ bool HMACAuth::setKey(const QUuid& uidKey) { bool HMACAuth::addData(const char* data, int dataLen) { QMutexLocker lock(&_lock); - return (bool) HMAC_Update(_hmacContext.get(), reinterpret_cast(data), dataLen); + return (bool) HMAC_Update(_hmacContext, reinterpret_cast(data), dataLen); } HMACAuth::HMACHash HMACAuth::result() { HMACHash hashValue(EVP_MAX_MD_SIZE); unsigned int hashLen; QMutexLocker lock(&_lock); - HMAC_Final(_hmacContext.get(), &hashValue[0], &hashLen); + HMAC_Final(_hmacContext, &hashValue[0], &hashLen); hashValue.resize((size_t) hashLen); // Clear state for possible reuse. - HMAC_Init_ex(_hmacContext.get(), nullptr, 0, nullptr, nullptr); + HMAC_Init_ex(_hmacContext, nullptr, 0, nullptr, nullptr); return hashValue; } diff --git a/libraries/networking/src/HMACAuth.h b/libraries/networking/src/HMACAuth.h index 57f9dd64b8..89c20a3906 100644 --- a/libraries/networking/src/HMACAuth.h +++ b/libraries/networking/src/HMACAuth.h @@ -26,14 +26,14 @@ public: explicit HMACAuth(AuthMethod authMethod = MD5); ~HMACAuth(); - bool setKey(const char * keyValue, int keyLen); + bool setKey(const char* keyValue, int keyLen); bool setKey(const QUuid& uidKey); - bool addData(const char * data, int dataLen); + bool addData(const char* data, int dataLen); HMACHash result(); private: QMutex _lock; - std::unique_ptr _hmacContext; + struct hmac_ctx_st * _hmacContext; AuthMethod _authMethod; }; From 29b4353397bdfa168d0d212738d333c8091e7392 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 18:26:32 -0700 Subject: [PATCH 18/48] Spacing clean-up --- libraries/networking/src/HMACAuth.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/HMACAuth.h b/libraries/networking/src/HMACAuth.h index 89c20a3906..0bf7a86ec1 100644 --- a/libraries/networking/src/HMACAuth.h +++ b/libraries/networking/src/HMACAuth.h @@ -33,7 +33,7 @@ public: private: QMutex _lock; - struct hmac_ctx_st * _hmacContext; + struct hmac_ctx_st* _hmacContext; AuthMethod _authMethod; }; From 16b0c48b73bb5d5afb31c2efc904328497dc84b8 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 18:32:25 -0700 Subject: [PATCH 19/48] HMACAuth - improved syntax for new --- libraries/networking/src/HMACAuth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp index baeffeadeb..fdc2588f62 100644 --- a/libraries/networking/src/HMACAuth.cpp +++ b/libraries/networking/src/HMACAuth.cpp @@ -29,7 +29,7 @@ HMACAuth::~HMACAuth() #else HMACAuth::HMACAuth(AuthMethod authMethod) - : _hmacContext(new(HMAC_CTX)) + : _hmacContext(new HMAC_CTX()) , _authMethod(authMethod) { HMAC_CTX_init(_hmacContext); } From 3e1a33377615fa097605125d6eea0ac1c22d3a2a Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 21 Mar 2018 18:40:54 -0700 Subject: [PATCH 20/48] Just bump default packet version --- .../networking/src/udt/PacketHeaders.cpp | 19 +++++++------- libraries/networking/src/udt/PacketHeaders.h | 25 ++++++------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index d6b59d59f9..f09a049fc4 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -25,29 +25,30 @@ int packetTypeMetaTypeId = qRegisterMetaType(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return static_cast(DomainListVersion::UseHmacAuthentication); + return static_cast(DomainListVersion::GetMachineFingerprintFromUUIDSupport); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::UseHmacAuthentication); + return static_cast(EntityVersion::ShadowControl); case PacketType::EntityQuery: - return static_cast(EntityQueryPacketVersion::UseHmacAuthentication); + return static_cast(EntityQueryPacketVersion::RemovedJurisdictions); case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::UseHmacAuthentication); + return static_cast(AvatarMixerPacketVersion::FBXReaderNodeReparenting); case PacketType::MessagesData: - return static_cast(MessageDataVersion::UseHmacAuthentication); + return static_cast(MessageDataVersion::TextOrBinaryData); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetMappingOperation: case PacketType::AssetMappingOperationReply: + return static_cast(AssetServerPacketVersion::RedirectedMappings); case PacketType::AssetGetInfo: case PacketType::AssetGet: case PacketType::AssetUpload: - return static_cast(AssetServerPacketVersion::UseHmacAuthentication); + return static_cast(AssetServerPacketVersion::RangeRequestSupport); case PacketType::NodeIgnoreRequest: return 18; // Introduction of node ignore request (which replaced an unused packet tpye) @@ -58,10 +59,10 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectRequestVersion::AlwaysHasMachineFingerprint); case PacketType::DomainServerAddedNode: - return static_cast(DomainServerAddedNodeVersion::UseHmacAuthentication); + return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); case PacketType::EntityScriptCallMethod: - return static_cast(EntityScriptCallMethodVersion::UseHmacAuthentication); + return static_cast(EntityScriptCallMethodVersion::ClientCallable); case PacketType::MixedAudio: case PacketType::SilentAudioFrame: @@ -69,7 +70,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::MicrophoneAudioNoEcho: case PacketType::MicrophoneAudioWithEcho: case PacketType::AudioStreamStats: - return static_cast(AudioVersion::UseHmacAuthentication); + return static_cast(AudioVersion::HighDynamicRangeVolume); case PacketType::ICEPing: return static_cast(IcePingVersion::SendICEPeerID); case PacketType::DomainSettings: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 9dc3f2befd..98a9087d37 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -231,29 +231,25 @@ enum class EntityVersion : PacketVersion { ZoneStageRemoved, SoftEntities, MaterialEntities, - ShadowControl, - UseHmacAuthentication + ShadowControl }; enum class EntityScriptCallMethodVersion : PacketVersion { ServerCallable = 18, - ClientCallable = 19, - UseHmacAuthentication = 20 + ClientCallable = 19 }; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, JSONFilterWithFamilyTree = 19, ConnectionIdentifier = 20, - RemovedJurisdictions = 21, - UseHmacAuthentication = 22 + RemovedJurisdictions = 21 }; enum class AssetServerPacketVersion: PacketVersion { VegasCongestionControl = 19, RangeRequestSupport, - RedirectedMappings, - UseHmacAuthentication + RedirectedMappings }; enum class AvatarMixerPacketVersion : PacketVersion { @@ -278,8 +274,7 @@ enum class AvatarMixerPacketVersion : PacketVersion { AvatarIdentityLookAtSnapping, UpdatedMannequinDefaultAvatar, AvatarJointDefaultPoseFlags, - FBXReaderNodeReparenting, - UseHmacAuthentication + FBXReaderNodeReparenting }; enum class DomainConnectRequestVersion : PacketVersion { @@ -299,16 +294,14 @@ enum class DomainConnectionDeniedVersion : PacketVersion { enum class DomainServerAddedNodeVersion : PacketVersion { PrePermissionsGrid = 17, - PermissionsGrid, - UseHmacAuthentication + PermissionsGrid }; enum class DomainListVersion : PacketVersion { PrePermissionsGrid = 18, PermissionsGrid, GetUsernameFromUUIDSupport, - GetMachineFingerprintFromUUIDSupport, - UseHmacAuthentication + GetMachineFingerprintFromUUIDSupport }; enum class AudioVersion : PacketVersion { @@ -319,12 +312,10 @@ enum class AudioVersion : PacketVersion { SpaceBubbleChanges, HasPersonalMute, HighDynamicRangeVolume, - UseHmacAuthentication, }; enum class MessageDataVersion : PacketVersion { - TextOrBinaryData = 18, - UseHmacAuthentication + TextOrBinaryData = 18 }; enum class IcePingVersion : PacketVersion { From 68ab0eed68dfe8b8b2b76472e8c0891d52e477ee Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 22 Mar 2018 14:44:25 -0700 Subject: [PATCH 21/48] HMACAuth - fix some more spacing issue --- libraries/networking/src/HMACAuth.cpp | 4 ++-- libraries/networking/src/LimitedNodeList.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/networking/src/HMACAuth.cpp b/libraries/networking/src/HMACAuth.cpp index fdc2588f62..42b5c48d93 100644 --- a/libraries/networking/src/HMACAuth.cpp +++ b/libraries/networking/src/HMACAuth.cpp @@ -1,6 +1,6 @@ // -// HMACAuth.cpp -// libraries/shared/src +// HMACAuth.cpp +// libraries/networking/src // // Created by Simon Walton on 3/19/2018. // Copyright 2018 High Fidelity, Inc. diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index eb70fbcbdf..64969862ee 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -131,17 +131,17 @@ public: // use sendUnreliablePacket to send an unreliable packet (that you do not need to move) // either to a node (via its active socket) or to a manual sockaddr qint64 sendUnreliablePacket(const NLPacket& packet, const Node& destinationNode); - qint64 sendUnreliablePacket(const NLPacket & packet, const HifiSockAddr & sockAddr, HMACAuth * hmacAuth = nullptr); + qint64 sendUnreliablePacket(const NLPacket& packet, const HifiSockAddr& sockAddr, HMACAuth* hmacAuth = nullptr); // use sendPacket to send a moved unreliable or reliable NL packet to a node's active socket or manual sockaddr qint64 sendPacket(std::unique_ptr packet, const Node& destinationNode); - qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr & sockAddr, HMACAuth * hmacAuth = nullptr); + qint64 sendPacket(std::unique_ptr packet, const HifiSockAddr& sockAddr, HMACAuth* hmacAuth = nullptr); // use sendUnreliableUnorderedPacketList to unreliably send separate packets from the packet list // either to a node's active socket or to a manual sockaddr qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const Node& destinationNode); - qint64 sendUnreliableUnorderedPacketList(NLPacketList & packetList, const HifiSockAddr & sockAddr, - HMACAuth * hmacAuth = nullptr); + qint64 sendUnreliableUnorderedPacketList(NLPacketList& packetList, const HifiSockAddr& sockAddr, + HMACAuth* hmacAuth = nullptr); // use sendPacketList to send reliable packet lists (ordered or unordered) to a node's active socket // or to a manual sock addr From 765688d4dafb3724df35b501f87b54a480ba6251 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 3 Apr 2018 17:40:07 -0700 Subject: [PATCH 22/48] add JSDoc package lock json to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index df91e0ca7b..8d92fe770b 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ interface/compiledResources # GPUCache interface/resources/GPUCache/* + +# package lock file for JSDoc tool +tools/jsdoc/package-lock.json From 22ac40040200f255ff647bd972df6fb4de9af603 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 3 Apr 2018 17:48:17 -0700 Subject: [PATCH 23/48] update electron version to 1.8.4 --- server-console/package-lock.json | 454 ++++++++++++++++--------------- server-console/package.json | 8 +- server-console/src/main.js | 6 +- 3 files changed, 243 insertions(+), 225 deletions(-) diff --git a/server-console/package-lock.json b/server-console/package-lock.json index e25fd3cded..4311fde51a 100644 --- a/server-console/package-lock.json +++ b/server-console/package-lock.json @@ -4,6 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.2.tgz", + "integrity": "sha512-A6Uv1anbsCvrRDtaUXS2xZ5tlzD+Kg7yMRlSLFDy3z0r7KlGXDzL14vELXIAgpk2aJbU3XeZZQRcEkLkowT92g==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -40,12 +46,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true - }, "array-find-index": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.1.tgz", @@ -148,12 +148,6 @@ "lru-cache": "4.0.1" } }, - "balanced-match": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz", - "integrity": "sha1-qRzdHr7xqGZZ5w/03vAWJfwtZ1Y=", - "dev": true - }, "base64-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", @@ -217,16 +211,6 @@ "hoek": "2.16.3" } }, - "brace-expansion": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", - "integrity": "sha1-Rr/1ARXUf8mriYVKu4fZgHihCZE=", - "dev": true, - "requires": { - "balanced-match": "0.3.0", - "concat-map": "0.0.1" - } - }, "buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", @@ -515,29 +499,70 @@ "jsbn": "0.1.0" } }, - "electron-download": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-2.1.1.tgz", - "integrity": "sha1-AH07HyrTco0nzP5PhJayY/kTijE=", + "electron": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.4.tgz", + "integrity": "sha512-2f1cx0G3riMFODXFftF5AHXy+oHfhpntZHTDN66Hxtl09gmEr42B3piNEod9MEmw72f75LX2JfeYceqq1PF8cA==", "dev": true, "requires": { - "debug": "2.2.0", - "home-path": "1.0.3", + "@types/node": "8.10.2", + "electron-download": "3.3.0", + "extract-zip": "1.5.0" + } + }, + "electron-download": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", + "integrity": "sha1-LP1U1pZsAZxNSa1l++Zcyc3vaMg=", + "dev": true, + "requires": { + "debug": "2.6.9", + "fs-extra": "0.30.0", + "home-path": "1.0.5", "minimist": "1.2.0", - "mkdirp": "0.5.1", - "mv": "2.1.1", - "nugget": "1.6.2", - "path-exists": "1.0.0", - "rc": "1.1.6" + "nugget": "2.0.1", + "path-exists": "2.1.0", + "rc": "1.1.6", + "semver": "5.5.0", + "sumchecker": "1.3.1" }, "dependencies": { "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "ms": "0.7.1" + "ms": "2.0.0" + } + }, + "fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "dev": true, + "requires": { + "graceful-fs": "4.1.3", + "jsonfile": "2.2.3", + "klaw": "1.3.1", + "path-is-absolute": "1.0.0", + "rimraf": "2.6.2" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "sumchecker": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-1.3.1.tgz", + "integrity": "sha1-ebs7RFbdBPGOvbwNcDodHa7FEF0=", + "dev": true, + "requires": { + "debug": "2.6.9", + "es6-promise": "4.2.4" } } } @@ -579,9 +604,9 @@ } }, "electron-packager": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-11.0.0.tgz", - "integrity": "sha512-ufyYMe3Gt6IEZm9RuG+KK3Nh+V2jZHWg9gihp8wylUNtleQihECIXtQdpPJxH9740XFERVPraNEaa7cZvDzpyw==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-12.0.0.tgz", + "integrity": "sha1-uC0k14ovIUA7v9FmpbFWmJTVzQw=", "dev": true, "requires": { "asar": "0.14.2", @@ -590,20 +615,25 @@ "electron-osx-sign": "0.4.8", "extract-zip": "1.5.0", "fs-extra": "5.0.0", + "galactus": "0.2.0", "get-package-info": "1.0.0", - "mz": "2.7.0", "nodeify": "1.0.1", "parse-author": "2.0.0", "pify": "3.0.0", "plist": "2.1.0", - "pruner": "0.0.7", - "rcedit": "0.9.0", + "rcedit": "1.0.0", "resolve": "1.5.0", "sanitize-filename": "1.6.1", "semver": "5.5.0", - "yargs-parser": "8.1.0" + "yargs-parser": "9.0.2" }, "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -726,6 +756,12 @@ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true }, + "rcedit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.0.0.tgz", + "integrity": "sha512-W7DNa34x/3OgWyDHsI172AG/Lr/lZ+PkavFkHj0QhhkBRcV9QTmRJE1tDKrWkx8XHPSBsmZkNv9OKue6pncLFQ==", + "dev": true + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -746,19 +782,18 @@ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", "dev": true + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } } } }, - "electron-prebuilt": { - "version": "0.37.5", - "resolved": "https://registry.npmjs.org/electron-prebuilt/-/electron-prebuilt-0.37.5.tgz", - "integrity": "sha1-OkGJgod4FdOnrB+bLi9KcPQg/3A=", - "dev": true, - "requires": { - "electron-download": "2.1.1", - "extract-zip": "1.5.0" - } - }, "end-of-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz", @@ -787,6 +822,12 @@ "is-arrayish": "0.2.1" } }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -873,6 +914,62 @@ } } }, + "flora-colossus": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-0.0.2.tgz", + "integrity": "sha1-fRvimh8X+k8isb1hSC+Gw04HuQE=", + "dev": true, + "requires": { + "debug": "3.1.0", + "fs-extra": "4.0.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "4.1.3", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -904,6 +1001,63 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "galactus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-0.2.0.tgz", + "integrity": "sha1-w9Y7pVAkZv5A6mfMaJCFs90kqPw=", + "dev": true, + "requires": { + "debug": "3.1.0", + "flora-colossus": "0.0.2", + "fs-extra": "4.0.3" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "4.1.3", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -1107,9 +1261,9 @@ "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" }, "home-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.3.tgz", - "integrity": "sha1-ns5Z/sPwMubRC1Q0/uJk30wt4y8=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.5.tgz", + "integrity": "sha1-eIspgVsS1Tus9XVkhHbm+QQdEz8=", "dev": true }, "hosted-git-info": { @@ -1461,15 +1615,6 @@ "mime-db": "1.22.0" } }, - "minimatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", - "integrity": "sha1-UjYVelHk8ATBd/s8Un/33Xjw74M=", - "dev": true, - "requires": { - "brace-expansion": "1.1.3" - } - }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", @@ -1723,61 +1868,9 @@ } }, "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", - "dev": true - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "dev": true, - "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "1.0.4", - "inherits": "2.0.1", - "minimatch": "3.0.0", - "once": "1.3.3", - "path-is-absolute": "1.0.0" - } - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "dev": true, - "requires": { - "glob": "6.0.4" - } - } - } - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "requires": { - "any-promise": "1.3.0", - "object-assign": "4.0.1", - "thenify-all": "1.6.0" - } - }, - "ncp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, "node-notifier": { @@ -1843,27 +1936,27 @@ } }, "nugget": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", - "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", + "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", "dev": true, "requires": { - "debug": "2.2.0", + "debug": "2.6.9", "minimist": "1.2.0", "pretty-bytes": "1.0.4", "progress-stream": "1.2.0", "request": "2.71.0", - "single-line-log": "0.4.1", + "single-line-log": "1.1.2", "throttleit": "0.0.2" }, "dependencies": { "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "ms": "0.7.1" + "ms": "2.0.0" } }, "throttleit": { @@ -1966,10 +2059,13 @@ } }, "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } }, "path-is-absolute": { "version": "1.0.0", @@ -2070,46 +2166,6 @@ "is-promise": "1.0.1" } }, - "pruner": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/pruner/-/pruner-0.0.7.tgz", - "integrity": "sha1-NF+8s+gHARY6HXrfVrrCKaWh5ME=", - "dev": true, - "requires": { - "fs-extra": "4.0.3" - }, - "dependencies": { - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "4.0.0", - "universalify": "0.1.1" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true, - "optional": true - } - } - } - } - }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -2153,12 +2209,6 @@ "strip-json-comments": "1.0.4" } }, - "rcedit": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-0.9.0.tgz", - "integrity": "sha1-ORDfVzRTmeKwMl9KUZAH+J5V7xw=", - "dev": true - }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -2288,10 +2338,13 @@ "dev": true }, "single-line-log": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", - "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", + "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", + "dev": true, + "requires": { + "string-width": "1.0.1" + } }, "sntp": { "version": "1.0.9", @@ -2476,24 +2529,6 @@ } } }, - "thenify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", - "dev": true, - "requires": { - "any-promise": "1.3.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "dev": true, - "requires": { - "thenify": "3.3.0" - } - }, "throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", @@ -2694,23 +2729,6 @@ "y18n": "3.2.1" } }, - "yargs-parser": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", - "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", - "dev": true, - "requires": { - "camelcase": "4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - } - } - }, "yauzl": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", diff --git a/server-console/package.json b/server-console/package.json index 0b13eeb2a8..2428d2574e 100644 --- a/server-console/package.json +++ b/server-console/package.json @@ -8,8 +8,8 @@ "" ], "devDependencies": { - "electron-packager": "^11.0.0", - "electron-prebuilt": "0.37.5" + "electron-packager": "^12.0.0", + "electron": "1.8.4" }, "repository": { "type": "git", @@ -25,6 +25,7 @@ "dependencies": { "always-tail": "0.2.0", "cheerio": "^0.19.0", + "electron-log": "1.1.1", "extend": "^3.0.0", "fs-extra": "^1.0.0", "node-notifier": "^5.2.1", @@ -32,7 +33,6 @@ "request": "^2.67.0", "request-progress": "1.0.2", "tar-fs": "^1.12.0", - "yargs": "^3.30.0", - "electron-log": "1.1.1" + "yargs": "^3.30.0" } } diff --git a/server-console/src/main.js b/server-console/src/main.js index efa04a8512..b08db6222f 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -8,9 +8,9 @@ const nativeImage = electron.nativeImage; const notifier = require('node-notifier'); const util = require('util'); const dialog = electron.dialog; -const Menu = require('menu'); -const Tray = require('tray'); -const shell = require('shell'); +const Menu = electron.Menu; +const Tray = electron.Tray; +const shell = electron.shell; const os = require('os'); const childProcess = require('child_process'); const path = require('path'); From c23d4df93237ba53373df2a7024849518ee93475 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 3 Apr 2018 17:54:26 -0700 Subject: [PATCH 24/48] use SIGTERM as default kill signal for child process bug --- server-console/src/modules/hf-process.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-console/src/modules/hf-process.js b/server-console/src/modules/hf-process.js index 767befec7b..797ee38a0d 100644 --- a/server-console/src/modules/hf-process.js +++ b/server-console/src/modules/hf-process.js @@ -226,7 +226,7 @@ Process.prototype = extend(Process.prototype, { } }); } else { - var signal = force ? 'SIGKILL' : null; + var signal = force ? 'SIGKILL' : 'SIGTERM'; this.child.kill(signal); } From 7638dceee39e8c3134f8cfdb92d32d2ea8fca934 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 3 Apr 2018 18:06:18 -0700 Subject: [PATCH 25/48] fix remote require from downloader page --- server-console/src/downloader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server-console/src/downloader.js b/server-console/src/downloader.js index f7e67f03ce..7dc9223802 100644 --- a/server-console/src/downloader.js +++ b/server-console/src/downloader.js @@ -2,7 +2,7 @@ function ready() { console.log("Ready"); const electron = require('electron'); - const remote = require('remote'); + const remote = electron.remote; window.$ = require('./vendor/jquery/jquery-2.1.4.min.js'); $(".state").hide(); From 3a8c9de4fb9feca5f6a0a713230d1f397074a10e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 4 Apr 2018 10:38:32 -0700 Subject: [PATCH 26/48] fix some xcode warnings for unnecessary captured this --- assignment-client/src/Agent.cpp | 2 +- domain-server/src/DomainServer.cpp | 2 +- libraries/entities-renderer/src/RenderableWebEntityItem.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 10b8d44545..1df901dd98 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -380,7 +380,7 @@ void Agent::executeScript() { using namespace recording; static const FrameType AVATAR_FRAME_TYPE = Frame::registerFrameType(AvatarData::FRAME_NAME); - Frame::registerFrameHandler(AVATAR_FRAME_TYPE, [this, scriptedAvatar](Frame::ConstPointer frame) { + Frame::registerFrameHandler(AVATAR_FRAME_TYPE, [scriptedAvatar](Frame::ConstPointer frame) { auto recordingInterface = DependencyManager::get(); bool useFrameSkeleton = recordingInterface->getPlayerUseSkeletonModel(); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index dbf2907cc0..5f19bbe46e 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2926,7 +2926,7 @@ void DomainServer::updateReplicatedNodes() { } auto nodeList = DependencyManager::get(); - nodeList->eachMatchingNode([this](const SharedNodePointer& otherNode) -> bool { + nodeList->eachMatchingNode([](const SharedNodePointer& otherNode) -> bool { return otherNode->getType() == NodeType::Agent; }, [this](const SharedNodePointer& otherNode) { auto shouldReplicate = shouldReplicateNode(*otherNode); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index bd00ded12d..4f072d40e3 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -266,7 +266,7 @@ bool WebEntityRenderer::buildWebSurface(const TypedEntityPointer& entity) { // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) _webSurface->setMaxFps(DEFAULT_MAX_FPS); - QObject::connect(_webSurface.data(), &OffscreenQmlSurface::rootContextCreated, [this](QQmlContext* surfaceContext) { + QObject::connect(_webSurface.data(), &OffscreenQmlSurface::rootContextCreated, [](QQmlContext* surfaceContext) { // FIXME - Keyboard HMD only: Possibly add "HMDinfo" object to context for WebView.qml. surfaceContext->setContextProperty("desktop", QVariant()); // Let us interact with the keyboard From 2546ff91ca4427de972968794fdccb55924dc9d4 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 28 Mar 2018 15:05:51 -0700 Subject: [PATCH 27/48] Fix nodelist connections not resetting on both ends This change adds a connection ID to ping packets. Each node keeps a connection id for each other node that it has connected to. When a node is removed the connection id is incremented. If a node sees another node with a higher connection id, it will reset its connection with the new connection id, ensuring that local state is reset on both ends when nodes lose contact. --- domain-server/src/DomainGatekeeper.cpp | 12 ++++++--- libraries/networking/src/LimitedNodeList.cpp | 27 +++++++++++++++----- libraries/networking/src/LimitedNodeList.h | 11 +++++--- libraries/networking/src/NodeList.cpp | 24 ++++++++++++++--- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 7d0b538f6e..9f24036e92 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -451,11 +451,12 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect return SharedNodePointer(); } - QUuid hintNodeID; + QUuid existingNodeID; // in case this is a node that's failing to connect // double check we don't have the same node whose sockets match exactly already in the list limitedNodeList->eachNodeBreakable([&](const SharedNodePointer& node){ + if (node->getPublicSocket() == nodeConnection.publicSockAddr && node->getLocalSocket() == nodeConnection.localSockAddr) { // we have a node that already has these exact sockets - this can occur if a node // is failing to connect to the domain @@ -465,15 +466,20 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect auto existingNodeData = static_cast(node->getLinkedData()); if (existingNodeData->getUsername() == username) { - hintNodeID = node->getUUID(); + qDebug() << "Deleting existing connection from same sockaddr: " << node->getUUID(); + existingNodeID = node->getUUID(); return false; } } return true; }); + if (!existingNodeID.isNull()) { + limitedNodeList->killNodeWithUUID(existingNodeID); + } + // add the connecting node (or re-use the matched one from eachNodeBreakable above) - SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID); + SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); // set the edit rights for this user newNode->setPermissions(userPerms); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 0803e380f2..e27e2d6d08 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -578,9 +578,10 @@ void LimitedNodeList::reset() { // we need to make sure any socket connections are gone so wait on that here _nodeSocket.clearConnections(); + _connectionIDs.clear(); } -bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID) { +bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID, ConnectionID newConnectionID) { QReadLocker readLocker(&_nodeMutex); NodeHash::iterator it = _nodeHash.find(nodeUUID); @@ -594,7 +595,7 @@ bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID) { _nodeHash.unsafe_erase(it); } - handleNodeKill(matchingNode); + handleNodeKill(matchingNode, newConnectionID); return true; } @@ -609,7 +610,7 @@ void LimitedNodeList::processKillNode(ReceivedMessage& message) { killNodeWithUUID(nodeUUID); } -void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) { +void LimitedNodeList::handleNodeKill(const SharedNodePointer& node, ConnectionID nextConnectionID) { qCDebug(networking) << "Killed" << *node; node->stopPingTimer(); emit nodeKilled(node); @@ -617,6 +618,15 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) { if (auto activeSocket = node->getActiveSocket()) { _nodeSocket.cleanupConnection(*activeSocket); } + + auto it = _connectionIDs.find(node->getUUID()); + if (it != _connectionIDs.end()) { + if (nextConnectionID == NULL_CONNECTION_ID) { + it->second++; + } else { + it->second = nextConnectionID; + } + } } SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, @@ -638,6 +648,11 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t return matchingNode; } else { + auto it = _connectionIDs.find(uuid); + if (it == _connectionIDs.end()) { + _connectionIDs[uuid] = INITIAL_CONNECTION_ID; + } + // we didn't have this node, so add them Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket); newNode->setIsReplicated(isReplicated); @@ -712,13 +727,13 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t } } -std::unique_ptr LimitedNodeList::constructPingPacket(PingType_t pingType) { - int packetSize = sizeof(PingType_t) + sizeof(quint64); +std::unique_ptr LimitedNodeList::constructPingPacket(const QUuid& nodeId, PingType_t pingType) { + int packetSize = sizeof(PingType_t) + sizeof(quint64) + sizeof(int64_t); auto pingPacket = NLPacket::create(PacketType::Ping, packetSize); - pingPacket->writePrimitive(pingType); pingPacket->writePrimitive(usecTimestampNow()); + pingPacket->writePrimitive(_connectionIDs[nodeId]); return pingPacket; } diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 7165b3dd63..7ec3a41450 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -66,6 +66,10 @@ const QHostAddress DEFAULT_ASSIGNMENT_CLIENT_MONITOR_HOSTNAME = QHostAddress::Lo const QString USERNAME_UUID_REPLACEMENT_STATS_KEY = "$username"; +using ConnectionID = int64_t; +const ConnectionID NULL_CONNECTION_ID { -1 }; +const ConnectionID INITIAL_CONNECTION_ID { 0 }; + typedef std::pair UUIDNodePair; typedef tbb::concurrent_unordered_map NodeHash; @@ -180,7 +184,7 @@ public: void getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond); void resetPacketStats(); - std::unique_ptr constructPingPacket(PingType_t pingType = PingType::Agnostic); + std::unique_ptr constructPingPacket(const QUuid& nodeId, PingType_t pingType = PingType::Agnostic); std::unique_ptr constructPingReplyPacket(ReceivedMessage& message); static std::unique_ptr constructICEPingPacket(PingType_t pingType, const QUuid& iceID); @@ -319,7 +323,7 @@ public slots: void startSTUNPublicSocketUpdate(); virtual void sendSTUNRequest(); - bool killNodeWithUUID(const QUuid& nodeUUID); + bool killNodeWithUUID(const QUuid& nodeUUID, ConnectionID newConnectionID = NULL_CONNECTION_ID); signals: void dataSent(quint8 channelType, int bytes); @@ -371,7 +375,7 @@ protected: bool packetSourceAndHashMatchAndTrackBandwidth(const udt::Packet& packet, Node* sourceNode = nullptr); void processSTUNResponse(std::unique_ptr packet); - void handleNodeKill(const SharedNodePointer& node); + void handleNodeKill(const SharedNodePointer& node, ConnectionID newConnectionID = NULL_CONNECTION_ID); void stopInitialSTUNUpdate(bool success); @@ -418,6 +422,7 @@ protected: } } + std::unordered_map _connectionIDs; private slots: void flagTimeForConnectionStep(ConnectionStep connectionStep, quint64 timestamp); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index cb0d2e4cd5..d33a81841a 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -214,6 +214,20 @@ void NodeList::processPingPacket(QSharedPointer message, Shared sendingNode->setSymmetricSocket(senderSockAddr); } } + + int64_t connectionId; + + message->readPrimitive(&connectionId); + + auto it = _connectionIDs.find(sendingNode->getUUID()); + if (it != _connectionIDs.end()) { + if (connectionId > it->second) { + qDebug() << "Received a ping packet with a larger connection id (" << connectionId << ">" << it->second << ") from " + << sendingNode->getUUID(); + killNodeWithUUID(sendingNode->getUUID(), connectionId); + } + } + } void NodeList::processPingReplyPacket(QSharedPointer message, SharedNodePointer sendingNode) { @@ -689,16 +703,18 @@ void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) { if (node->getConnectionAttempts() > 0 && node->getConnectionAttempts() % NUM_DEBUG_CONNECTION_ATTEMPTS == 0) { qCDebug(networking) << "No response to UDP hole punch pings for node" << node->getUUID() << "in last second."; } + + auto nodeId = node->getUUID(); // send the ping packet to the local and public sockets for this node - auto localPingPacket = constructPingPacket(PingType::Local); + auto localPingPacket = constructPingPacket(nodeId, PingType::Local); sendPacket(std::move(localPingPacket), *node, node->getLocalSocket()); - auto publicPingPacket = constructPingPacket(PingType::Public); + auto publicPingPacket = constructPingPacket(nodeId, PingType::Public); sendPacket(std::move(publicPingPacket), *node, node->getPublicSocket()); if (!node->getSymmetricSocket().isNull()) { - auto symmetricPingPacket = constructPingPacket(PingType::Symmetric); + auto symmetricPingPacket = constructPingPacket(nodeId, PingType::Symmetric); sendPacket(std::move(symmetricPingPacket), *node, node->getSymmetricSocket()); } @@ -768,7 +784,7 @@ void NodeList::sendKeepAlivePings() { auto type = node->getType(); return !node->isUpstream() && _nodeTypesOfInterest.contains(type) && !NodeType::isDownstream(type); }, [&](const SharedNodePointer& node) { - sendPacket(constructPingPacket(), *node); + sendPacket(constructPingPacket(node->getUUID()), *node); }); } From e37655ecdcbdaea5c186e823eaebde1e782e2673 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 11 Apr 2018 16:35:49 +1200 Subject: [PATCH 28/48] Update location.hostname JSDoc per serverless domains --- libraries/networking/src/AddressManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index b42aec2771..6609efcf30 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -39,7 +39,7 @@ const QString GET_PLACE = "/api/v1/places/%1"; * @property {Uuid} domainId - Synonym for domainId. Read-only. Deprecated: This property * is deprecated and will soon be removed. * @property {string} hostname - The name of the domain for your current metaverse address (e.g., "AvatarIsland", - * localhost, or an IP address). + * localhost, an IP address, or the file path to a serverless domain). * Read-only. * @property {string} href - Your current metaverse address (e.g., "hifi://avatarisland/15,-10,26/0,0,0,1") * regardless of whether or not you're connected to the domain. From c836014d213ad81db87e0274da4dd23cf68b13ad Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 12 Apr 2018 09:24:10 +1200 Subject: [PATCH 29/48] Fix JSDoc typo noticed in passing --- interface/src/scripting/WindowScriptingInterface.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index e3b092d011..2d92c945f2 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -528,8 +528,8 @@ private slots: signals: /**jsdoc - * Triggered when you change the domain you're visiting. Warning: Is not emitted if you go to domain that - * isn't running. + * Triggered when you change the domain you're visiting. Warning: Is not emitted if you go to a domain + * that isn't running. * @function Window.domainChanged * @param {string} domainURL - The domain's URL. * @returns {Signal} From bd59ff1573dc949f2937f68818a205a0263fd54e Mon Sep 17 00:00:00 2001 From: Clement Date: Fri, 13 Apr 2018 15:04:25 -0700 Subject: [PATCH 30/48] Fix threaded force crashes --- interface/src/Menu.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index df7546cd33..64ea9659ef 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -749,32 +749,32 @@ Menu::Menu() { action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread([]() { crash::pureVirtualCall(); }); }); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree); connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread([]() { crash::doubleFree(); }); }); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort); connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread([]() { crash::doAbort(); }); }); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference); connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread([]() { crash::nullDeref(); }); }); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess); connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread([]() { crash::outOfBoundsVectorCrash(); }); }); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault); connect(action, &QAction::triggered, qApp, []() { crash::newFault(); }); action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread([]() { crash::newFault(); }); }); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); // Developer > Log... addActionToQMenuAndActionHash(developerMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L, From 5e22202355b1e386abe4972842f1fb1ae9d51c44 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Mon, 16 Apr 2018 13:11:03 +0300 Subject: [PATCH 31/48] Material resources: support relative path for emissiveMap --- .../model-networking/src/model-networking/MaterialCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/model-networking/src/model-networking/MaterialCache.cpp b/libraries/model-networking/src/model-networking/MaterialCache.cpp index f0cbfc914a..823602d939 100644 --- a/libraries/model-networking/src/model-networking/MaterialCache.cpp +++ b/libraries/model-networking/src/model-networking/MaterialCache.cpp @@ -194,7 +194,7 @@ std::pair> NetworkMaterialResource } else if (key == "emissiveMap") { auto value = materialJSON.value(key); if (value.isString()) { - material->setEmissiveMap(value.toString()); + material->setEmissiveMap(baseUrl.resolved(value.toString())); } } else if (key == "albedoMap") { auto value = materialJSON.value(key); From e9d291257677c20944148faae66a22497c6000ff Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 16 Apr 2018 12:03:58 -0700 Subject: [PATCH 32/48] Update variable naming in NodeList from Id to ID --- libraries/networking/src/NodeList.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index d33a81841a..e099a2c527 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -215,16 +215,16 @@ void NodeList::processPingPacket(QSharedPointer message, Shared } } - int64_t connectionId; + int64_t connectionID; - message->readPrimitive(&connectionId); + message->readPrimitive(&connectionID); auto it = _connectionIDs.find(sendingNode->getUUID()); if (it != _connectionIDs.end()) { - if (connectionId > it->second) { - qDebug() << "Received a ping packet with a larger connection id (" << connectionId << ">" << it->second << ") from " + if (connectionID > it->second) { + qDebug() << "Received a ping packet with a larger connection id (" << connectionID << ">" << it->second << ") from " << sendingNode->getUUID(); - killNodeWithUUID(sendingNode->getUUID(), connectionId); + killNodeWithUUID(sendingNode->getUUID(), connectionID); } } @@ -704,17 +704,17 @@ void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) { qCDebug(networking) << "No response to UDP hole punch pings for node" << node->getUUID() << "in last second."; } - auto nodeId = node->getUUID(); + auto nodeID = node->getUUID(); // send the ping packet to the local and public sockets for this node - auto localPingPacket = constructPingPacket(nodeId, PingType::Local); + auto localPingPacket = constructPingPacket(nodeID, PingType::Local); sendPacket(std::move(localPingPacket), *node, node->getLocalSocket()); - auto publicPingPacket = constructPingPacket(nodeId, PingType::Public); + auto publicPingPacket = constructPingPacket(nodeID, PingType::Public); sendPacket(std::move(publicPingPacket), *node, node->getPublicSocket()); if (!node->getSymmetricSocket().isNull()) { - auto symmetricPingPacket = constructPingPacket(nodeId, PingType::Symmetric); + auto symmetricPingPacket = constructPingPacket(nodeID, PingType::Symmetric); sendPacket(std::move(symmetricPingPacket), *node, node->getSymmetricSocket()); } From 93cf399fd6667ea74108af842dd3b81b75d60766 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 16 Apr 2018 15:07:45 -0700 Subject: [PATCH 33/48] Only dismiss letterbox when clicking on grey areas --- .../resources/qml/hifi/LetterboxMessage.qml | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index d88bf1b147..c0f3fa1006 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -135,9 +135,46 @@ Item { } } } + // Left gray MouseArea MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton + anchors.left: parent.left; + anchors.right: textContainer.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton; + onClicked: { + letterbox.visible = false; + } + } + // Right gray MouseArea + MouseArea { + anchors.left: textContainer.left; + anchors.right: parent.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton; + onClicked: { + letterbox.visible = false; + } + } + // Top gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: textContainer.top; + acceptedButtons: Qt.LeftButton; + onClicked: { + letterbox.visible = false; + } + } + // Bottom gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: textContainer.bottom; + anchors.bottom: parent.bottom; + acceptedButtons: Qt.LeftButton; onClicked: { letterbox.visible = false; } From f36a5289c9ac6a79880dc0cc10bb84daf3608929 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 17 Apr 2018 14:33:11 +1200 Subject: [PATCH 34/48] Add Entities materialData JSDoc --- libraries/entities/src/EntityItemProperties.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index f78168b05d..4638b46437 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -695,8 +695,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @typedef {object} Entities.EntityProperties-Material * @property {string} materialURL="" - URL to a {@link MaterialResource}. If you append ?name to the URL, the * material with that name in the {@link MaterialResource} will be applied to the entity.
- * Alternatively, set the property value to "userData" to use the {@link Entities.EntityProperties|userData} - * entity property to live edit the material resource values. + * Alternatively, set the property value to "materialData" to use the materialData property + * for the {@link MaterialResource} values. * @property {number} priority=0 - The priority for applying the material to its parent. Only the highest priority material is * applied, with materials of the same priority randomly assigned. Materials that come with the model have a priority of * 0. @@ -710,6 +710,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * { x: 0, y: 0 }{ x: 1, y: 1 }. * @property {Vec2} materialMappingScale=1,1 - How much to scale the material within the parent's UV-space. * @property {number} materialMappingRot=0 - How much to rotate the material within the parent's UV-space, in degrees. + * @property {string} materialData="" - Used to store {@link MaterialResource} data as a JSON string. You can use + * JSON.parse() to parse the string into a JavaScript object which you can manipulate the properties of, and + * use JSON.stringify() to convert the object into a string to put in the property. * @example Color a sphere using a Material entity. * var entityID = Entities.addEntity({ * type: "Sphere", @@ -722,13 +725,14 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * var materialID = Entities.addEntity({ * type: "Material", * parentID: entityID, - * materialURL: "userData", + * materialURL: "materialData", * priority: 1, - * userData: JSON.stringify({ + * materialData: JSON.stringify({ + * materialVersion: 1, * materials: { * // Can only set albedo on a Shape entity. * // Value overrides entity's "color" property. - * albedo: [1.0, 0, 0] + * albedo: [1.0, 1.0, 0] // Yellow * } * }), * }); From a626e9f6f31f4bf1dd30c8bf9c5e4aa14889cdba Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 17 Apr 2018 15:09:10 -0700 Subject: [PATCH 35/48] Add special handler for heap corruption --- interface/src/Crashpad.cpp | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/interface/src/Crashpad.cpp b/interface/src/Crashpad.cpp index 8ed3fc23bd..c824aaa9e4 100644 --- a/interface/src/Crashpad.cpp +++ b/interface/src/Crashpad.cpp @@ -31,10 +31,40 @@ using namespace crashpad; static const std::string BACKTRACE_URL { CMAKE_BACKTRACE_URL }; static const std::string BACKTRACE_TOKEN { CMAKE_BACKTRACE_TOKEN }; +static std::wstring gIPCPipe; + extern QString qAppFileName(); // crashpad::AnnotationList* crashpadAnnotations { nullptr }; +#include + +LONG WINAPI vectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { + if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xe06d7363) { // external exception + return EXCEPTION_CONTINUE_SEARCH; + } + + if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000374) { // heap corruption + qCritical() << "VectoredExceptionHandler"; + qCritical() << QString::number(pExceptionInfo->ExceptionRecord->ExceptionCode, 16); + qCritical() << "Heap corruption!"; + + CrashpadClient client; + if (gIPCPipe.length()) { + + bool rc = client.SetHandlerIPCPipe(gIPCPipe); + qCritical() << "SetHandlerIPCPipe = " << rc; + } else { + qCritical() << "No IPC Pipe was previously defined for crash handler."; + } + qCritical() << "Calling DumpAndCrash()"; + client.DumpAndCrash(pExceptionInfo); + return EXCEPTION_CONTINUE_SEARCH; + } + + return EXCEPTION_CONTINUE_SEARCH; +} + bool startCrashHandler() { if (BACKTRACE_URL.empty() || BACKTRACE_TOKEN.empty()) { return false; @@ -76,7 +106,12 @@ bool startCrashHandler() { // Enable automated uploads. database->GetSettings()->SetUploadsEnabled(true); - return client.StartHandler(handler, db, db, BACKTRACE_URL, annotations, arguments, true, true); + bool result = client.StartHandler(handler, db, db, BACKTRACE_URL, annotations, arguments, true, true); + gIPCPipe = client.GetHandlerIPCPipe(); + + AddVectoredExceptionHandler(0, vectoredExceptionHandler); + + return result; } void setCrashAnnotation(std::string name, std::string value) { From e6f0bfbf4cc7b50a94123d06c9048ea2ede5d696 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 18 Apr 2018 00:10:49 +0200 Subject: [PATCH 36/48] get proper checkbox value from the allow light selection menu option --- scripts/system/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index fd7a488eb7..88e2fd7bf0 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1293,7 +1293,7 @@ Script.scriptEnding.connect(function () { Settings.setValue(SETTING_EDIT_PREFIX + MENU_CREATE_ENTITIES_GRABBABLE, Menu.isOptionChecked(MENU_CREATE_ENTITIES_GRABBABLE)); Settings.setValue(SETTING_EDIT_PREFIX + MENU_ALLOW_SELECTION_LARGE, Menu.isOptionChecked(MENU_ALLOW_SELECTION_LARGE)); Settings.setValue(SETTING_EDIT_PREFIX + MENU_ALLOW_SELECTION_SMALL, Menu.isOptionChecked(MENU_ALLOW_SELECTION_SMALL)); - Settings.setValue(SETTING_EDIT_PREFIX + MENU_ALLOW_SELECTION_LIGHTS, Menu.isOptionChecked(GRABBABLE_ENTITIES_MENU_ITEM)); + Settings.setValue(SETTING_EDIT_PREFIX + MENU_ALLOW_SELECTION_LIGHTS, Menu.isOptionChecked(MENU_ALLOW_SELECTION_LIGHTS)); progressDialog.cleanup(); From 40e311fe1ee640c68ab87eeae71aa1ffff44e74f Mon Sep 17 00:00:00 2001 From: MiladNazeri Date: Tue, 17 Apr 2018 15:20:05 -0700 Subject: [PATCH 37/48] Fix QT typo --- BUILD_LINUX.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 64c0e9a643..0daef5ae05 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -11,11 +11,11 @@ Should you choose not to install Qt5 via a package manager that handles dependen ## Ubuntu 16.04 specific build guide ### Prepare environment - +hifiqt5.10.1 Install qt: ```bash -wget http://debian.highfidelity.com/pool/h/hi/hifi-qt5.10.1_5.10.1_amd64.deb -sudo dpkg -i hifi-qt5.10.1_5.10.1_amd64.deb +wget http://debian.highfidelity.com/pool/h/hi/hifiqt5.10.1_5.10.1_amd64.deb +sudo dpkg -i hifiqt5.10.1_5.10.1_amd64.deb ``` Install build dependencies: From 7e6d45ead783a6acbb94ba9ccce03ad4344c4779 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 17 Apr 2018 16:22:42 -0700 Subject: [PATCH 38/48] CR --- interface/src/Crashpad.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/interface/src/Crashpad.cpp b/interface/src/Crashpad.cpp index c824aaa9e4..27da619af1 100644 --- a/interface/src/Crashpad.cpp +++ b/interface/src/Crashpad.cpp @@ -40,18 +40,19 @@ extern QString qAppFileName(); #include LONG WINAPI vectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { - if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xe06d7363) { // external exception + static const DWORD EXTERNAL_EXCEPTION_CODE{ 0xe06d7363 }; + static const DWORD HEAP_CORRUPTION_CODE{ 0xc0000374 }; + + auto exceptionCode = pExceptionInfo->ExceptionRecord->ExceptionCode; + if (exceptionCode == EXTERNAL_EXCEPTION_CODE) { return EXCEPTION_CONTINUE_SEARCH; } - if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000374) { // heap corruption - qCritical() << "VectoredExceptionHandler"; - qCritical() << QString::number(pExceptionInfo->ExceptionRecord->ExceptionCode, 16); - qCritical() << "Heap corruption!"; + if (exceptionCode == HEAP_CORRUPTION_CODE) { + qCritical() << "VectoredExceptionHandler: Heap corruption:" << QString::number(exceptionCode, 16); CrashpadClient client; if (gIPCPipe.length()) { - bool rc = client.SetHandlerIPCPipe(gIPCPipe); qCritical() << "SetHandlerIPCPipe = " << rc; } else { From 2e8a6e1961eec3f2c240cd28518504a64ef9c4bd Mon Sep 17 00:00:00 2001 From: Clement Date: Tue, 10 Apr 2018 18:49:30 -0700 Subject: [PATCH 39/48] Remove more dead octree code --- .../src/entities/EntityTreeSendThread.h | 2 - .../src/octree/OctreeSendThread.cpp | 177 ++-- .../src/octree/OctreeSendThread.h | 10 +- assignment-client/src/octree/OctreeServer.cpp | 4 - assignment-client/src/octree/OctreeServer.h | 2 +- libraries/entities/src/EntityItem.h | 1 - libraries/entities/src/EntityTree.h | 5 - libraries/entities/src/EntityTreeElement.cpp | 449 --------- libraries/entities/src/EntityTreeElement.h | 11 - libraries/entities/src/LightEntityItem.cpp | 1 - libraries/entities/src/LineEntityItem.cpp | 1 - libraries/entities/src/LineEntityItem.h | 1 - libraries/entities/src/MaterialEntityItem.cpp | 2 - libraries/entities/src/MaterialEntityItem.h | 1 - libraries/entities/src/ModelEntityItem.cpp | 3 +- libraries/entities/src/ModelEntityItem.h | 1 - .../entities/src/ParticleEffectEntityItem.cpp | 2 - libraries/entities/src/PolyLineEntityItem.cpp | 2 - libraries/entities/src/PolyLineEntityItem.h | 1 - libraries/entities/src/PolyVoxEntityItem.cpp | 2 - libraries/entities/src/PolyVoxEntityItem.h | 1 - libraries/entities/src/ShapeEntityItem.cpp | 2 - libraries/entities/src/TextEntityItem.cpp | 2 - libraries/entities/src/TextEntityItem.h | 1 - libraries/entities/src/WebEntityItem.cpp | 2 - libraries/entities/src/WebEntityItem.h | 1 - libraries/entities/src/ZoneEntityItem.cpp | 2 - libraries/entities/src/ZoneEntityItem.h | 1 - libraries/octree/src/Octree.cpp | 849 ------------------ libraries/octree/src/Octree.h | 103 +-- libraries/octree/src/OctreeElement.cpp | 27 - libraries/octree/src/OctreeElement.h | 13 - libraries/octree/src/OctreeElementBag.cpp | 40 - libraries/octree/src/OctreeElementBag.h | 22 - libraries/octree/src/OctreeProcessor.cpp | 2 +- libraries/octree/src/OctreeQueryNode.cpp | 69 -- libraries/octree/src/OctreeQueryNode.h | 31 - libraries/octree/src/OctreeSceneStats.cpp | 4 - libraries/octree/src/OctreeSceneStats.h | 3 - libraries/shared/src/OctalCode.cpp | 16 - libraries/shared/src/OctalCode.h | 2 - tests/entities/src/main.cpp | 1 - 42 files changed, 66 insertions(+), 1806 deletions(-) delete mode 100644 libraries/octree/src/OctreeElementBag.cpp diff --git a/assignment-client/src/entities/EntityTreeSendThread.h b/assignment-client/src/entities/EntityTreeSendThread.h index 594f423838..1e2bd15429 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.h +++ b/assignment-client/src/entities/EntityTreeSendThread.h @@ -48,8 +48,6 @@ private: void preDistributionProcessing() override; bool hasSomethingToSend(OctreeQueryNode* nodeData) override { return !_sendQueue.empty(); } bool shouldStartNewTraversal(OctreeQueryNode* nodeData, bool viewFrustumChanged) override { return viewFrustumChanged || _traversal.finished(); } - void preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene) override {}; - bool shouldTraverseAndSend(OctreeQueryNode* nodeData) override { return true; } DiffTraversal _traversal; EntityPriorityQueue _sendQueue; diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 715e83f403..f8d566862e 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -304,23 +304,6 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* return numPackets; } -void OctreeSendThread::preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene) { - // If we're starting a full scene, then definitely we want to empty the elementBag - if (isFullScene) { - nodeData->elementBag.deleteAll(); - } - - // This is the start of "resending" the scene. - bool dontRestartSceneOnMove = false; // this is experimental - if (dontRestartSceneOnMove) { - if (nodeData->elementBag.isEmpty()) { - nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); - } - } else { - nodeData->elementBag.insert(_myServer->getOctree()->getRoot()); - } -} - /// Version of octree element distributor that sends the deepest LOD level at once int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged) { OctreeServer::didPacketDistributor(this); @@ -366,16 +349,8 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // the current view frustum for things to send. if (shouldStartNewTraversal(nodeData, viewFrustumChanged)) { - // if our view has changed, we need to reset these things... - if (viewFrustumChanged) { - if (nodeData->moveShouldDump() || nodeData->hasLodChanged()) { - nodeData->dumpOutOfView(); - } - } - // track completed scenes and send out the stats packet accordingly nodeData->stats.sceneCompleted(); - nodeData->setLastRootTimestamp(_myServer->getOctree()->getRoot()->getLastChanged()); _myServer->getOctree()->releaseSceneEncodeData(&nodeData->extraEncodeData); // TODO: add these to stats page @@ -389,111 +364,74 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // TODO: add these to stats page //::startSceneSleepTime = _usleepTime; - nodeData->sceneStart(usecTimestampNow() - CHANGE_FUDGE); // start tracking our stats nodeData->stats.sceneStarted(isFullScene, viewFrustumChanged, _myServer->getOctree()->getRoot()); - - preStartNewScene(nodeData, isFullScene); } - // If we have something in our elementBag, then turn them into packets and send them out... - if (shouldTraverseAndSend(nodeData)) { - quint64 start = usecTimestampNow(); + quint64 start = usecTimestampNow(); - _myServer->getOctree()->withReadLock([&]{ - traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); - }); + _myServer->getOctree()->withReadLock([&]{ + traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); + }); - // Here's where we can/should allow the server to send other data... - // send the environment packet - // TODO: should we turn this into a while loop to better handle sending multiple special packets - if (_myServer->hasSpecialPacketsToSend(node) && !nodeData->isShuttingDown()) { - int specialPacketsSent = 0; - int specialBytesSent = _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); - nodeData->resetOctreePacket(); // because nodeData's _sequenceNumber has changed - _truePacketsSent += specialPacketsSent; - _trueBytesSent += specialBytesSent; - _packetsSentThisInterval += specialPacketsSent; + // Here's where we can/should allow the server to send other data... + // send the environment packet + // TODO: should we turn this into a while loop to better handle sending multiple special packets + if (_myServer->hasSpecialPacketsToSend(node) && !nodeData->isShuttingDown()) { + int specialPacketsSent = 0; + int specialBytesSent = _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); + nodeData->resetOctreePacket(); // because nodeData's _sequenceNumber has changed + _truePacketsSent += specialPacketsSent; + _trueBytesSent += specialBytesSent; + _packetsSentThisInterval += specialPacketsSent; - _totalPackets += specialPacketsSent; - _totalBytes += specialBytesSent; + _totalPackets += specialPacketsSent; + _totalBytes += specialBytesSent; - _totalSpecialPackets += specialPacketsSent; - _totalSpecialBytes += specialBytesSent; + _totalSpecialPackets += specialPacketsSent; + _totalSpecialBytes += specialBytesSent; + } + + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + + // Re-send packets that were nacked by the client + while (nodeData->hasNextNackedPacket() && _packetsSentThisInterval < maxPacketsPerInterval) { + const NLPacket* packet = nodeData->getNextNackedPacket(); + if (packet) { + DependencyManager::get()->sendUnreliablePacket(*packet, *node); + int numBytes = packet->getDataSize(); + _truePacketsSent++; + _trueBytesSent += numBytes; + _packetsSentThisInterval++; + + _totalPackets++; + _totalBytes += numBytes; + _totalWastedBytes += udt::MAX_PACKET_SIZE - packet->getDataSize(); } + } - // calculate max number of packets that can be sent during this interval - int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); - int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + quint64 end = usecTimestampNow(); + int elapsedmsec = (end - start) / USECS_PER_MSEC; + OctreeServer::trackLoopTime(elapsedmsec); - // Re-send packets that were nacked by the client - while (nodeData->hasNextNackedPacket() && _packetsSentThisInterval < maxPacketsPerInterval) { - const NLPacket* packet = nodeData->getNextNackedPacket(); - if (packet) { - DependencyManager::get()->sendUnreliablePacket(*packet, *node); - int numBytes = packet->getDataSize(); - _truePacketsSent++; - _trueBytesSent += numBytes; - _packetsSentThisInterval++; + // if after sending packets we've emptied our bag, then we want to remember that we've sent all + // the octree elements from the current view frustum + if (!hasSomethingToSend(nodeData)) { + nodeData->setViewSent(true); - _totalPackets++; - _totalBytes += numBytes; - _totalWastedBytes += udt::MAX_PACKET_SIZE - packet->getDataSize(); - } + // If this was a full scene then make sure we really send out a stats packet at this point so that + // the clients will know the scene is stable + if (isFullScene) { + nodeData->stats.sceneCompleted(); + handlePacketSend(node, nodeData, true); } - - quint64 end = usecTimestampNow(); - int elapsedmsec = (end - start) / USECS_PER_MSEC; - OctreeServer::trackLoopTime(elapsedmsec); - - // if after sending packets we've emptied our bag, then we want to remember that we've sent all - // the octree elements from the current view frustum - if (!hasSomethingToSend(nodeData)) { - nodeData->updateLastKnownViewFrustum(); - nodeData->setViewSent(true); - - // If this was a full scene then make sure we really send out a stats packet at this point so that - // the clients will know the scene is stable - if (isFullScene) { - nodeData->stats.sceneCompleted(); - handlePacketSend(node, nodeData, true); - } - } - - } // end if bag wasn't empty, and so we sent stuff... + } return _truePacketsSent; } -bool OctreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) { - bool somethingToSend = false; - OctreeQueryNode* nodeData = static_cast(params.nodeData); - if (!nodeData->elementBag.isEmpty()) { - quint64 encodeStart = usecTimestampNow(); - quint64 lockWaitStart = encodeStart; - - _myServer->getOctree()->withReadLock([&]{ - OctreeServer::trackTreeWaitTime((float)(usecTimestampNow() - lockWaitStart)); - - OctreeElementPointer subTree = nodeData->elementBag.extract(); - if (subTree) { - // NOTE: this is where the tree "contents" are actually packed - nodeData->stats.encodeStarted(); - _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params); - nodeData->stats.encodeStopped(); - - somethingToSend = true; - } - }); - - OctreeServer::trackEncodeTime((float)(usecTimestampNow() - encodeStart)); - } else { - OctreeServer::trackTreeWaitTime(OctreeServer::SKIP_TIME); - OctreeServer::trackEncodeTime(OctreeServer::SKIP_TIME); - } - return somethingToSend; -} - void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { // calculate max number of packets that can be sent during this interval int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); @@ -502,21 +440,12 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre int extraPackingAttempts = 0; // init params once outside the while loop - int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust(); - int boundaryLevelAdjust = boundaryLevelAdjustClient + - (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - float octreeSizeScale = nodeData->getOctreeSizeScale(); - EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, - viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, - isFullScene, nodeData); + EncodeBitstreamParams params(WANT_EXISTS_BITS, nodeData); // Our trackSend() function is implemented by the server subclass, and will be called back as new entities/data elements are sent params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) { _myServer->trackSend(dataID, dataEdited, _nodeUuid); }; nodeData->copyCurrentViewFrustum(params.viewFrustum); - if (viewFrustumChanged) { - nodeData->copyLastKnownViewFrustum(params.lastViewFrustum); - } bool somethingToSend = true; // assume we have something bool hadSomething = hasSomethingToSend(nodeData); @@ -537,7 +466,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre } // If the bag had contents but is now empty then we know we've sent the entire scene. - bool completedScene = hadSomething && nodeData->elementBag.isEmpty(); + bool completedScene = hadSomething; if (completedScene || lastNodeDidntFit) { // we probably want to flush what has accumulated in nodeData but: // do we have more data to send? and is there room? diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index 220952e209..91c0ec7adc 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -54,7 +54,7 @@ protected: virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene); - virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters); + virtual bool traverseTreeAndBuildNextPacketPayload(EncodeBitstreamParams& params, const QJsonObject& jsonFilters) = 0; OctreePacketData _packetData; QWeakPointer _node; @@ -63,14 +63,12 @@ protected: private: /// Called before a packetDistributor pass to allow for pre-distribution processing - virtual void preDistributionProcessing() {}; + virtual void preDistributionProcessing() = 0; int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false); int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged); - virtual bool hasSomethingToSend(OctreeQueryNode* nodeData) { return !nodeData->elementBag.isEmpty(); } - virtual bool shouldStartNewTraversal(OctreeQueryNode* nodeData, bool viewFrustumChanged) { return viewFrustumChanged || !hasSomethingToSend(nodeData); } - virtual void preStartNewScene(OctreeQueryNode* nodeData, bool isFullScene); - virtual bool shouldTraverseAndSend(OctreeQueryNode* nodeData) { return hasSomethingToSend(nodeData); } + virtual bool hasSomethingToSend(OctreeQueryNode* nodeData) = 0; + virtual bool shouldStartNewTraversal(OctreeQueryNode* nodeData, bool viewFrustumChanged) = 0; int _truePacketsSent { 0 }; // available for debug stats int _trueBytesSent { 0 }; // available for debug stats diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 0dbd24fe9c..fad2c1f026 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -876,10 +876,6 @@ void OctreeServer::parsePayload() { } } -OctreeServer::UniqueSendThread OctreeServer::newSendThread(const SharedNodePointer& node) { - return std::unique_ptr(new OctreeSendThread(this, node)); -} - OctreeServer::UniqueSendThread OctreeServer::createSendThread(const SharedNodePointer& node) { auto sendThread = newSendThread(node); diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index e7efc731f2..b25e537d70 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -174,7 +174,7 @@ protected: void beginRunning(QByteArray replaceData); UniqueSendThread createSendThread(const SharedNodePointer& node); - virtual UniqueSendThread newSendThread(const SharedNodePointer& node); + virtual UniqueSendThread newSendThread(const SharedNodePointer& node) = 0; int _argc; const char** _argv; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index de98c1a47a..0557bbe5ad 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -120,7 +120,6 @@ public: void markAsChangedOnServer(); quint64 getLastChangedOnServer() const; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; virtual OctreeElement::AppendState appendEntityData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 791c030fc8..a080801a0e 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -88,7 +88,6 @@ public: // These methods will allow the OctreeServer to send your tree inbound edit packets of your // own definition. Implement these to allow your octree based server to support editing - virtual bool getWantSVOfileVersions() const override { return true; } virtual PacketType expectedDataPacketType() const override { return PacketType::EntityData; } virtual bool handlesEditPacketType(PacketType packetType) const override; void fixupTerseEditLogging(EntityItemProperties& properties, QList& changedProperties); @@ -107,11 +106,7 @@ public: virtual bool rootElementHasData() const override { return true; } - // the root at least needs to store the number of entities in the packet/buffer - virtual int minimumRequiredRootDataBytes() const override { return sizeof(uint16_t); } - virtual bool suppressEmptySubtrees() const override { return false; } virtual void releaseSceneEncodeData(OctreeElementExtraEncodeData* extraEncodeData) const override; - virtual bool mustIncludeAllChildData() const override { return false; } virtual void update() override { update(true); } diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 1ae55bc333..cbcddfc57b 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -67,455 +67,6 @@ void EntityTreeElement::debugExtraEncodeData(EncodeBitstreamParams& params) cons } } -void EntityTreeElement::initializeExtraEncodeData(EncodeBitstreamParams& params) { - - auto entityNodeData = static_cast(params.nodeData); - assert(entityNodeData); - - OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; - - assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes - // Check to see if this element yet has encode data... if it doesn't create it - if (!extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData { new EntityTreeElementExtraEncodeData() }; - entityTreeElementExtraEncodeData->elementCompleted = (_entityItems.size() == 0); - for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { - EntityTreeElementPointer child = getChildAtIndex(i); - if (!child) { - entityTreeElementExtraEncodeData->childCompleted[i] = true; // if no child exists, it is completed - } else { - if (child->hasEntities()) { - entityTreeElementExtraEncodeData->childCompleted[i] = false; // HAS ENTITIES NEEDS ENCODING - } else { - entityTreeElementExtraEncodeData->childCompleted[i] = true; // child doesn't have enities, it is completed - } - } - } - forEachEntity([&](EntityItemPointer entity) { - entityTreeElementExtraEncodeData->entities.insert(entity->getEntityItemID(), entity->getEntityProperties(params)); - }); - - // TODO: some of these inserts might be redundant!!! - extraEncodeData->insert(this, entityTreeElementExtraEncodeData); - } -} - -bool EntityTreeElement::shouldIncludeChildData(int childIndex, EncodeBitstreamParams& params) const { - - auto entityNodeData = static_cast(params.nodeData); - assert(entityNodeData); - - OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; - assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes - - if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData - = std::static_pointer_cast((*extraEncodeData)[this]); - - bool childCompleted = entityTreeElementExtraEncodeData->childCompleted[childIndex]; - - // If we haven't completely sent the child yet, then we should include it - return !childCompleted; - } - - // I'm not sure this should ever happen, since we should have the extra encode data if we're considering - // the child data for this element - assert(false); - return false; -} - -bool EntityTreeElement::shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { - EntityTreeElementPointer childElement = getChildAtIndex(childIndex); - if (childElement->alreadyFullyEncoded(params)) { - return false; - } - - return true; // if we don't know otherwise than recurse! -} - -bool EntityTreeElement::alreadyFullyEncoded(EncodeBitstreamParams& params) const { - auto entityNodeData = static_cast(params.nodeData); - assert(entityNodeData); - - OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; - assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes - - if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData - = std::static_pointer_cast((*extraEncodeData)[this]); - - // If we know that ALL subtrees below us have already been recursed, then we don't - // need to recurse this child. - return entityTreeElementExtraEncodeData->subtreeCompleted; - } - return false; -} - -void EntityTreeElement::updateEncodedData(int childIndex, AppendState childAppendState, EncodeBitstreamParams& params) const { - auto entityNodeData = static_cast(params.nodeData); - assert(entityNodeData); - - OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; - assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes - - if (extraEncodeData->contains(this)) { - EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData - = std::static_pointer_cast((*extraEncodeData)[this]); - - if (childAppendState == OctreeElement::COMPLETED) { - entityTreeElementExtraEncodeData->childCompleted[childIndex] = true; - } - } else { - assert(false); // this shouldn't happen! - } -} - - - - -void EntityTreeElement::elementEncodeComplete(EncodeBitstreamParams& params) const { - const bool wantDebug = false; - - if (wantDebug) { - qCDebug(entities) << "EntityTreeElement::elementEncodeComplete() element:" << _cube; - } - - auto entityNodeData = static_cast(params.nodeData); - assert(entityNodeData); - - OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; - assert(extraEncodeData); // EntityTrees always require extra encode data on their encoding passes - assert(extraEncodeData->contains(this)); - - EntityTreeElementExtraEncodeDataPointer thisExtraEncodeData - = std::static_pointer_cast((*extraEncodeData)[this]); - - // Note: this will be called when OUR element has finished running through encodeTreeBitstreamRecursion() - // which means, it's possible that our parent element hasn't finished encoding OUR data... so - // in this case, our children may be complete, and we should clean up their encode data... - // but not necessarily cleanup our own encode data... - // - // If we're really complete here's what must be true... - // 1) our own data must be complete - // 2) the data for all our immediate children must be complete. - // However, the following might also be the case... - // 1) it's ok for our child trees to not yet be fully encoded/complete... - // SO LONG AS... the our child's node is in the bag ready for encoding - - bool someChildTreeNotComplete = false; - for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { - EntityTreeElementPointer childElement = getChildAtIndex(i); - if (childElement) { - - // why would this ever fail??? - // If we've encoding this element before... but we're coming back a second time in an attempt to - // encode our parent... this might happen. - if (extraEncodeData->contains(childElement.get())) { - EntityTreeElementExtraEncodeDataPointer childExtraEncodeData - = std::static_pointer_cast((*extraEncodeData)[childElement.get()]); - - if (wantDebug) { - qCDebug(entities) << "checking child: " << childElement->_cube; - qCDebug(entities) << " childElement->isLeaf():" << childElement->isLeaf(); - qCDebug(entities) << " childExtraEncodeData->elementCompleted:" << childExtraEncodeData->elementCompleted; - qCDebug(entities) << " childExtraEncodeData->subtreeCompleted:" << childExtraEncodeData->subtreeCompleted; - } - - if (childElement->isLeaf() && childExtraEncodeData->elementCompleted) { - if (wantDebug) { - qCDebug(entities) << " CHILD IS LEAF -- AND CHILD ELEMENT DATA COMPLETED!!!"; - } - childExtraEncodeData->subtreeCompleted = true; - } - - if (!childExtraEncodeData->elementCompleted || !childExtraEncodeData->subtreeCompleted) { - someChildTreeNotComplete = true; - } - } - } - } - - if (wantDebug) { - qCDebug(entities) << "for this element: " << _cube; - qCDebug(entities) << " WAS elementCompleted:" << thisExtraEncodeData->elementCompleted; - qCDebug(entities) << " WAS subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; - } - - thisExtraEncodeData->subtreeCompleted = !someChildTreeNotComplete; - - if (wantDebug) { - qCDebug(entities) << " NOW elementCompleted:" << thisExtraEncodeData->elementCompleted; - qCDebug(entities) << " NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; - - if (thisExtraEncodeData->subtreeCompleted) { - qCDebug(entities) << " YEAH!!!!! >>>>>>>>>>>>>> NOW subtreeCompleted:" << thisExtraEncodeData->subtreeCompleted; - } - } -} - -OctreeElement::AppendState EntityTreeElement::appendElementData(OctreePacketData* packetData, - EncodeBitstreamParams& params) const { - - OctreeElement::AppendState appendElementState = OctreeElement::COMPLETED; // assume the best... - - auto entityNodeData = static_cast(params.nodeData); - Q_ASSERT_X(entityNodeData, "EntityTreeElement::appendElementData", "expected params.nodeData not to be null"); - - // first, check the params.extraEncodeData to see if there's any partial re-encode data for this element - OctreeElementExtraEncodeData* extraEncodeData = &entityNodeData->extraEncodeData; - - EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData = NULL; - bool hadElementExtraData = false; - if (extraEncodeData && extraEncodeData->contains(this)) { - entityTreeElementExtraEncodeData = - std::static_pointer_cast((*extraEncodeData)[this]); - hadElementExtraData = true; - } else { - // if there wasn't one already, then create one - entityTreeElementExtraEncodeData.reset(new EntityTreeElementExtraEncodeData()); - entityTreeElementExtraEncodeData->elementCompleted = !hasContent(); - - for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { - EntityTreeElementPointer child = getChildAtIndex(i); - if (!child) { - entityTreeElementExtraEncodeData->childCompleted[i] = true; // if no child exists, it is completed - } else { - if (child->hasEntities()) { - entityTreeElementExtraEncodeData->childCompleted[i] = false; - } else { - // if the child doesn't have enities, it is completed - entityTreeElementExtraEncodeData->childCompleted[i] = true; - } - } - } - forEachEntity([&](EntityItemPointer entity) { - entityTreeElementExtraEncodeData->entities.insert(entity->getEntityItemID(), entity->getEntityProperties(params)); - }); - } - - //assert(extraEncodeData); - //assert(extraEncodeData->contains(this)); - //entityTreeElementExtraEncodeData = std::static_pointer_cast((*extraEncodeData)[this]); - - LevelDetails elementLevel = packetData->startLevel(); - - // write our entities out... first determine which of the entities are in view based on our params - uint16_t numberOfEntities = 0; - uint16_t actualNumberOfEntities = 0; - int numberOfEntitiesOffset = 0; - withReadLock([&] { - QVector indexesOfEntitiesToInclude; - - // It's possible that our element has been previous completed. In this case we'll simply not include any of our - // entities for encoding. This is needed because we encode the element data at the "parent" level, and so we - // need to handle the case where our sibling elements need encoding but we don't. - if (!entityTreeElementExtraEncodeData->elementCompleted) { - - - // we have an EntityNodeData instance - // so we should assume that means we might have JSON filters to check - auto jsonFilters = entityNodeData->getJSONParameters(); - - - for (uint16_t i = 0; i < _entityItems.size(); i++) { - EntityItemPointer entity = _entityItems[i]; - bool includeThisEntity = true; - - if (!params.forceSendScene && entity->getLastChangedOnServer() < entityNodeData->getLastTimeBagEmpty()) { - includeThisEntity = false; - } - - // if this entity has been updated since our last full send and there are json filters, check them - if (includeThisEntity && !jsonFilters.isEmpty()) { - - // if params include JSON filters, check if this entity matches - bool entityMatchesFilters = entity->matchesJSONFilters(jsonFilters); - - if (entityMatchesFilters) { - // make sure this entity is in the set of entities sent last frame - entityNodeData->insertSentFilteredEntity(entity->getID()); - } else if (entityNodeData->sentFilteredEntity(entity->getID())) { - // this entity matched in the previous frame - we send it still so the client realizes it just - // fell outside of their filter - entityNodeData->removeSentFilteredEntity(entity->getID()); - } else if (!entityNodeData->isEntityFlaggedAsExtra(entity->getID())) { - // we don't send this entity because - // (1) it didn't match our filter - // (2) it didn't match our filter last frame - // (3) it isn't one the JSON query flags told us we should still include - includeThisEntity = false; - } - } - - if (includeThisEntity && hadElementExtraData) { - includeThisEntity = entityTreeElementExtraEncodeData->entities.contains(entity->getEntityItemID()); - } - - // we only check the bounds against our frustum and LOD if the query has asked us to check against the frustum - // which can sometimes not be the case when JSON filters are sent - if (entityNodeData->getUsesFrustum() && (includeThisEntity || params.recurseEverything)) { - - // we want to use the maximum possible box for this, so that we don't have to worry about the nuance of - // simulation changing what's visible. consider the case where the entity contains an angular velocity - // the entity may not be in view and then in view a frame later, let the client side handle it's view - // frustum culling on rendering. - bool success; - AACube entityCube = entity->getQueryAACube(success); - if (!success || !params.viewFrustum.cubeIntersectsKeyhole(entityCube)) { - includeThisEntity = false; // out of view, don't include it - } else { - // Check the size of the entity, it's possible that a "too small to see" entity is included in a - // larger octree cell because of its position (for example if it crosses the boundary of a cell it - // pops to the next higher cell. So we want to check to see that the entity is large enough to be seen - // before we consider including it. - success = true; - // we can't cull a parent-entity by its dimensions because the child may be larger. we need to - // avoid sending details about a child but not the parent. the parent's queryAACube should have - // been adjusted to encompass the queryAACube of the child. - AABox entityBounds = entity->hasChildren() ? AABox(entityCube) : entity->getAABox(success); - if (!success) { - // if this entity is a child of an avatar, the entity-server wont be able to determine its - // AABox. If this happens, fall back to the queryAACube. - entityBounds = AABox(entityCube); - } - auto renderAccuracy = calculateRenderAccuracy(params.viewFrustum.getPosition(), - entityBounds, - params.octreeElementSizeScale, - params.boundaryLevelAdjust); - if (renderAccuracy <= 0.0f) { - includeThisEntity = false; // too small, don't include it - - #ifdef WANT_LOD_DEBUGGING - qCDebug(entities) << "skipping entity - TOO SMALL - \n" - << "......id:" << entity->getID() << "\n" - << "....name:" << entity->getName() << "\n" - << "..bounds:" << entityBounds << "\n" - << "....cell:" << getAACube(); - #endif - } - } - } - - if (includeThisEntity) { - #ifdef WANT_LOD_DEBUGGING - qCDebug(entities) << "including entity - \n" - << "......id:" << entity->getID() << "\n" - << "....name:" << entity->getName() << "\n" - << "....cell:" << getAACube(); - #endif - indexesOfEntitiesToInclude << i; - numberOfEntities++; - } else { - // if the extra data included this entity, and we've decided to not include the entity, then - // we can treat it as if it was completed. - entityTreeElementExtraEncodeData->entities.remove(entity->getEntityItemID()); - } - } - } - - numberOfEntitiesOffset = packetData->getUncompressedByteOffset(); - bool successAppendEntityCount = packetData->appendValue(numberOfEntities); - - if (successAppendEntityCount) { - foreach(uint16_t i, indexesOfEntitiesToInclude) { - EntityItemPointer entity = _entityItems[i]; - LevelDetails entityLevel = packetData->startLevel(); - OctreeElement::AppendState appendEntityState = entity->appendEntityData(packetData, - params, entityTreeElementExtraEncodeData); - - // If none of this entity data was able to be appended, then discard it - // and don't include it in our entity count - if (appendEntityState == OctreeElement::NONE) { - packetData->discardLevel(entityLevel); - } else { - // If either ALL or some of it got appended, then end the level (commit it) - // and include the entity in our final count of entities - packetData->endLevel(entityLevel); - actualNumberOfEntities++; - - // If the entity item got completely appended, then we can remove it from the extra encode data - if (appendEntityState == OctreeElement::COMPLETED) { - entityTreeElementExtraEncodeData->entities.remove(entity->getEntityItemID()); - } - } - - // If any part of the entity items didn't fit, then the element is considered partial - // NOTE: if the entity item didn't fit or only partially fit, then the entity item should have - // added itself to the extra encode data. - if (appendEntityState != OctreeElement::COMPLETED) { - appendElementState = OctreeElement::PARTIAL; - } - } - } else { - // we we couldn't add the entity count, then we couldn't add anything for this element and we're in a NONE state - appendElementState = OctreeElement::NONE; - } - }); - - // If we were provided with extraEncodeData, and we allocated and/or got entityTreeElementExtraEncodeData - // then we need to do some additional processing, namely make sure our extraEncodeData is up to date for - // this octree element. - if (extraEncodeData && entityTreeElementExtraEncodeData) { - - // After processing, if we are PARTIAL or COMPLETED then we need to re-include our extra data. - // Only our parent can remove our extra data in these cases and only after it knows that all of its - // children have been encoded. - // - // FIXME -- this comment seems wrong.... - // - // If we weren't able to encode ANY data about ourselves, then we go ahead and remove our element data - // since that will signal that the entire element needs to be encoded on the next attempt - if (appendElementState == OctreeElement::NONE) { - - if (!entityTreeElementExtraEncodeData->elementCompleted && entityTreeElementExtraEncodeData->entities.size() == 0) { - // TODO: we used to delete the extra encode data here. But changing the logic around - // this is now a dead code branch. Clean this up! - } else { - // TODO: some of these inserts might be redundant!!! - extraEncodeData->insert(this, entityTreeElementExtraEncodeData); - } - } else { - - // If we weren't previously completed, check to see if we are - if (!entityTreeElementExtraEncodeData->elementCompleted) { - // If all of our items have been encoded, then we are complete as an element. - if (entityTreeElementExtraEncodeData->entities.size() == 0) { - entityTreeElementExtraEncodeData->elementCompleted = true; - } - } - - // TODO: some of these inserts might be redundant!!! - extraEncodeData->insert(this, entityTreeElementExtraEncodeData); - } - } - - // Determine if no entities at all were able to fit - bool noEntitiesFit = (numberOfEntities > 0 && actualNumberOfEntities == 0); - - // If we wrote fewer entities than we expected, update the number of entities in our packet - bool successUpdateEntityCount = true; - if (numberOfEntities != actualNumberOfEntities) { - successUpdateEntityCount = packetData->updatePriorBytes(numberOfEntitiesOffset, - (const unsigned char*)&actualNumberOfEntities, sizeof(actualNumberOfEntities)); - } - - // If we weren't able to update our entity count, or we couldn't fit any entities, then - // we should discard our element and return a result of NONE - if (!successUpdateEntityCount) { - packetData->discardLevel(elementLevel); - appendElementState = OctreeElement::NONE; - } else { - if (noEntitiesFit) { - //appendElementState = OctreeElement::PARTIAL; - packetData->discardLevel(elementLevel); - appendElementState = OctreeElement::NONE; - } else { - packetData->endLevel(elementLevel); - } - } - return appendElementState; -} - bool EntityTreeElement::containsEntityBounds(EntityItemPointer entity) const { bool success; auto queryCube = entity->getQueryAACube(success); diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index a56af5d03f..76e1e40812 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -121,17 +121,6 @@ public: virtual bool requiresSplit() const override { return false; } virtual void debugExtraEncodeData(EncodeBitstreamParams& params) const override; - virtual void initializeExtraEncodeData(EncodeBitstreamParams& params) override; - virtual bool shouldIncludeChildData(int childIndex, EncodeBitstreamParams& params) const override; - virtual bool shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const override; - virtual void updateEncodedData(int childIndex, AppendState childAppendState, EncodeBitstreamParams& params) const override; - virtual void elementEncodeComplete(EncodeBitstreamParams& params) const override; - - bool alreadyFullyEncoded(EncodeBitstreamParams& params) const; - - /// Override to serialize the state of this element. This is used for persistance and for transmission across the network. - virtual OctreeElement::AppendState appendElementData(OctreePacketData* packetData, - EncodeBitstreamParams& params) const override; /// Override to deserialize the state of this element. This is used for loading from a persisted file or from reading /// from the network. diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index 3f7fc5f799..f0fbb20f98 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -186,7 +186,6 @@ int LightEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, } -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags LightEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_IS_SPOTLIGHT; diff --git a/libraries/entities/src/LineEntityItem.cpp b/libraries/entities/src/LineEntityItem.cpp index 00196d7b23..92a1c25970 100644 --- a/libraries/entities/src/LineEntityItem.cpp +++ b/libraries/entities/src/LineEntityItem.cpp @@ -128,7 +128,6 @@ int LineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, } -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags LineEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_COLOR; diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 375453e0e9..84f9acf5f5 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -26,7 +26,6 @@ class LineEntityItem : public EntityItem { virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/MaterialEntityItem.cpp b/libraries/entities/src/MaterialEntityItem.cpp index 6c040296a3..489ba5772c 100644 --- a/libraries/entities/src/MaterialEntityItem.cpp +++ b/libraries/entities/src/MaterialEntityItem.cpp @@ -89,8 +89,6 @@ int MaterialEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* da return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags MaterialEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_MATERIAL_URL; diff --git a/libraries/entities/src/MaterialEntityItem.h b/libraries/entities/src/MaterialEntityItem.h index 969eb577ff..30743850dd 100644 --- a/libraries/entities/src/MaterialEntityItem.h +++ b/libraries/entities/src/MaterialEntityItem.h @@ -32,7 +32,6 @@ public: virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index bec7bc1906..be62664ff9 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -144,7 +144,6 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, return bytesRead; } -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); @@ -721,4 +720,4 @@ bool ModelEntityItem::isAnimatingSomething() const { _animationProperties.getRunning() && (_animationProperties.getFPS() != 0.0f); }); -} \ No newline at end of file +} diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index c2109ba51f..327606ae2f 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -30,7 +30,6 @@ public: virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 7d27011c56..d9ef5e2178 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -503,8 +503,6 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); diff --git a/libraries/entities/src/PolyLineEntityItem.cpp b/libraries/entities/src/PolyLineEntityItem.cpp index 498d13058e..420c570e8d 100644 --- a/libraries/entities/src/PolyLineEntityItem.cpp +++ b/libraries/entities/src/PolyLineEntityItem.cpp @@ -216,8 +216,6 @@ int PolyLineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* da return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags PolyLineEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_COLOR; diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 2dc8befe97..871c451c50 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -26,7 +26,6 @@ class PolyLineEntityItem : public EntityItem { virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index 84ce83d3a1..ed3372818a 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -183,8 +183,6 @@ int PolyVoxEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* dat return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags PolyVoxEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_VOXEL_VOLUME_SIZE; diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 0ddfe3e8cc..4dfe7b9535 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -26,7 +26,6 @@ class PolyVoxEntityItem : public EntityItem { virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 99f18d2dac..520d892682 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -188,8 +188,6 @@ int ShapeEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags ShapeEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_SHAPE; diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index 7030a95562..97080d3ca2 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -98,8 +98,6 @@ int TextEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags TextEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_TEXT; diff --git a/libraries/entities/src/TextEntityItem.h b/libraries/entities/src/TextEntityItem.h index 06b377ee14..efdc84bcd8 100644 --- a/libraries/entities/src/TextEntityItem.h +++ b/libraries/entities/src/TextEntityItem.h @@ -30,7 +30,6 @@ public: virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 548bca3225..f3159ba3f8 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -83,8 +83,6 @@ int WebEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, i return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags WebEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); requestedProperties += PROP_SOURCE_URL; diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index dab7cd5e22..1179f22ded 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -29,7 +29,6 @@ public: virtual EntityItemProperties getProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags()) const override; virtual bool setProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 4ae020f966..b07d0597bc 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -191,8 +191,6 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, return bytesRead; } - -// TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 2c6b01fc69..3a9c7cb1e6 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -33,7 +33,6 @@ public: virtual bool setProperties(const EntityItemProperties& properties) override; virtual bool setSubClassProperties(const EntityItemProperties& properties) override; - // TODO: eventually only include properties changed since the params.nodeData->getLastTimeBagEmpty() time virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index dafdfd5bf4..43883f68f3 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -45,7 +45,6 @@ #include "Octree.h" #include "OctreeConstants.h" -#include "OctreeElementBag.h" #include "OctreeLogging.h" #include "OctreeQueryNode.h" #include "OctreeUtils.h" @@ -57,7 +56,6 @@ Octree::Octree(bool shouldReaverage) : _rootElement(NULL), _isDirty(true), _shouldReaverage(shouldReaverage), - _stopImport(false), _isViewing(false), _isServer(false) { @@ -490,131 +488,6 @@ void Octree::readBitstreamToTree(const unsigned char * bitstream, uint64_t buffe // skip bitstream to new startPoint bitstreamAt += theseBytesRead; bytesRead += theseBytesRead; - - if (args.wantImportProgress) { - emit importProgress((100 * (bitstreamAt - bitstream)) / bufferSizeBytes); - } - } -} - -void Octree::deleteOctreeElementAt(float x, float y, float z, float s) { - unsigned char* octalCode = pointToOctalCode(x,y,z,s); - deleteOctalCodeFromTree(octalCode); - delete[] octalCode; // cleanup memory -} - -class DeleteOctalCodeFromTreeArgs { -public: - bool collapseEmptyTrees; - const unsigned char* codeBuffer; - int lengthOfCode; - bool deleteLastChild; - bool pathChanged; -}; - -// Note: uses the codeColorBuffer format, but the color's are ignored, because -// this only finds and deletes the element from the tree. -void Octree::deleteOctalCodeFromTree(const unsigned char* codeBuffer, bool collapseEmptyTrees) { - // recurse the tree while decoding the codeBuffer, once you find the element in question, recurse - // back and implement color reaveraging, and marking of lastChanged - DeleteOctalCodeFromTreeArgs args; - args.collapseEmptyTrees = collapseEmptyTrees; - args.codeBuffer = codeBuffer; - args.lengthOfCode = numberOfThreeBitSectionsInCode(codeBuffer); - args.deleteLastChild = false; - args.pathChanged = false; - - withWriteLock([&] { - deleteOctalCodeFromTreeRecursion(_rootElement, &args); - }); -} - -void Octree::deleteOctalCodeFromTreeRecursion(const OctreeElementPointer& element, void* extraData) { - DeleteOctalCodeFromTreeArgs* args = (DeleteOctalCodeFromTreeArgs*)extraData; - - int lengthOfElementCode = numberOfThreeBitSectionsInCode(element->getOctalCode()); - - // Since we traverse the tree in code order, we know that if our code - // matches, then we've reached our target element. - if (lengthOfElementCode == args->lengthOfCode) { - // we've reached our target, depending on how we're called we may be able to operate on it - // it here, we need to recurse up, and delete it there. So we handle these cases the same to keep - // the logic consistent. - args->deleteLastChild = true; - return; - } - - // Ok, we know we haven't reached our target element yet, so keep looking - int childIndex = branchIndexWithDescendant(element->getOctalCode(), args->codeBuffer); - OctreeElementPointer childElement = element->getChildAtIndex(childIndex); - - // If there is no child at the target location, and the current parent element is a colored leaf, - // then it means we were asked to delete a child out of a larger leaf voxel. - // We support this by breaking up the parent voxel into smaller pieces. - if (!childElement && element->requiresSplit()) { - // we need to break up ancestors until we get to the right level - OctreeElementPointer ancestorElement = element; - while (true) { - int index = branchIndexWithDescendant(ancestorElement->getOctalCode(), args->codeBuffer); - - // we end up with all the children, even the one we want to delete - ancestorElement->splitChildren(); - - int lengthOfAncestorElement = numberOfThreeBitSectionsInCode(ancestorElement->getOctalCode()); - - // If we've reached the parent of the target, then stop breaking up children - if (lengthOfAncestorElement == (args->lengthOfCode - 1)) { - - // since we created all the children when we split, we need to delete this target one - ancestorElement->deleteChildAtIndex(index); - break; - } - ancestorElement = ancestorElement->getChildAtIndex(index); - } - _isDirty = true; - args->pathChanged = true; - - // ends recursion, unwinds up stack - return; - } - - // if we don't have a child and we reach this point, then we actually know that the parent - // isn't a colored leaf, and the child branch doesn't exist, so there's nothing to do below and - // we can safely return, ending the recursion and unwinding - if (!childElement) { - return; - } - - // If we got this far then we have a child for the branch we're looking for, but we're not there yet - // recurse till we get there - deleteOctalCodeFromTreeRecursion(childElement, args); - - // If the lower level determined it needs to be deleted, then we should delete now. - if (args->deleteLastChild) { - element->deleteChildAtIndex(childIndex); // note: this will track dirtiness and lastChanged for this element - - // track our tree dirtiness - _isDirty = true; - - // track that path has changed - args->pathChanged = true; - - // If we're in collapseEmptyTrees mode, and this was the last child of this element, then we also want - // to delete this element. This will collapse the empty tree above us. - if (args->collapseEmptyTrees && element->getChildCount() == 0) { - // Can't delete the root this way. - if (element == _rootElement) { - args->deleteLastChild = false; // reset so that further up the unwinding chain we don't do anything - } - } else { - args->deleteLastChild = false; // reset so that further up the unwinding chain we don't do anything - } - } - - // If the lower level did some work, then we need to let this element know, so it can - // do any bookkeeping it wants to, like color re-averaging, time stamp marking, etc - if (args->pathChanged) { - element->handleSubtreeChanged(shared_from_this()); } } @@ -883,720 +756,6 @@ OctreeElementPointer Octree::getElementEnclosingPoint(const glm::vec3& point, Oc return args.element; } - - -int Octree::encodeTreeBitstream(const OctreeElementPointer& element, - OctreePacketData* packetData, OctreeElementBag& bag, - EncodeBitstreamParams& params) { - - // How many bytes have we written so far at this level; - int bytesWritten = 0; - - // you can't call this without a valid element - if (!element) { - qCDebug(octree, "WARNING! encodeTreeBitstream() called with element=NULL"); - params.stopReason = EncodeBitstreamParams::NULL_NODE; - return bytesWritten; - } - - // you can't call this without a valid nodeData - auto octreeQueryNode = static_cast(params.nodeData); - if (!octreeQueryNode) { - qCDebug(octree, "WARNING! encodeTreeBitstream() called with nodeData=NULL"); - params.stopReason = EncodeBitstreamParams::NULL_NODE_DATA; - return bytesWritten; - } - - // If we're at a element that is out of view, then we can return, because no nodes below us will be in view! - if (octreeQueryNode->getUsesFrustum() && !params.recurseEverything && !element->isInView(params.viewFrustum)) { - params.stopReason = EncodeBitstreamParams::OUT_OF_VIEW; - return bytesWritten; - } - - // write the octal code - bool roomForOctalCode = false; // assume the worst - int codeLength = 1; // assume root - if (params.chopLevels) { - unsigned char* newCode = chopOctalCode(element->getOctalCode(), params.chopLevels); - roomForOctalCode = packetData->startSubTree(newCode); - - if (newCode) { - codeLength = numberOfThreeBitSectionsInCode(newCode); - delete[] newCode; - } else { - codeLength = 1; - } - } else { - roomForOctalCode = packetData->startSubTree(element->getOctalCode()); - codeLength = (int)bytesRequiredForCodeLength(numberOfThreeBitSectionsInCode(element->getOctalCode())); - } - - // If the octalcode couldn't fit, then we can return, because no nodes below us will fit... - if (!roomForOctalCode) { - bag.insert(element); - params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - return bytesWritten; - } - - bytesWritten += codeLength; // keep track of byte count - - int currentEncodeLevel = 0; - - // record some stats, this is the one element that we won't record below in the recursion function, so we need to - // track it here - octreeQueryNode->stats.traversed(element); - - ViewFrustum::intersection parentLocationThisView = ViewFrustum::INTERSECT; // assume parent is in view, but not fully - - int childBytesWritten = encodeTreeBitstreamRecursion(element, packetData, bag, params, - currentEncodeLevel, parentLocationThisView); - - // if childBytesWritten == 1 then something went wrong... that's not possible - assert(childBytesWritten != 1); - - // if childBytesWritten == 2, then it can only mean that the lower level trees don't exist or for some - // reason couldn't be written... so reset them here... This isn't true for the non-color included case - if (suppressEmptySubtrees() && childBytesWritten == 2) { - childBytesWritten = 0; - //params.stopReason = EncodeBitstreamParams::UNKNOWN; // possibly should be DIDNT_FIT... - } - - // if we wrote child bytes, then return our result of all bytes written - if (childBytesWritten) { - bytesWritten += childBytesWritten; - } else { - // otherwise... if we didn't write any child bytes, then pretend like we also didn't write our octal code - bytesWritten = 0; - //params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - } - - if (bytesWritten == 0) { - packetData->discardSubTree(); - } else { - packetData->endSubTree(); - } - - return bytesWritten; -} - -int Octree::encodeTreeBitstreamRecursion(const OctreeElementPointer& element, - OctreePacketData* packetData, OctreeElementBag& bag, - EncodeBitstreamParams& params, int& currentEncodeLevel, - const ViewFrustum::intersection& parentLocationThisView) const { - - - const bool wantDebug = false; - - // The append state of this level/element. - OctreeElement::AppendState elementAppendState = OctreeElement::COMPLETED; // assume the best - - // How many bytes have we written so far at this level; - int bytesAtThisLevel = 0; - - // you can't call this without a valid element - if (!element) { - qCDebug(octree, "WARNING! encodeTreeBitstreamRecursion() called with element=NULL"); - params.stopReason = EncodeBitstreamParams::NULL_NODE; - return bytesAtThisLevel; - } - - // you can't call this without a valid nodeData - auto octreeQueryNode = static_cast(params.nodeData); - if (!octreeQueryNode) { - qCDebug(octree, "WARNING! encodeTreeBitstream() called with nodeData=NULL"); - params.stopReason = EncodeBitstreamParams::NULL_NODE_DATA; - return bytesAtThisLevel; - } - - - // Keep track of how deep we've encoded. - currentEncodeLevel++; - - params.maxLevelReached = std::max(currentEncodeLevel, params.maxLevelReached); - - // If we've reached our max Search Level, then stop searching. - if (currentEncodeLevel >= params.maxEncodeLevel) { - params.stopReason = EncodeBitstreamParams::TOO_DEEP; - return bytesAtThisLevel; - } - - ViewFrustum::intersection nodeLocationThisView = ViewFrustum::INSIDE; // assume we're inside - if (octreeQueryNode->getUsesFrustum() && !params.recurseEverything) { - float boundaryDistance = boundaryDistanceForRenderLevel(element->getLevel() + params.boundaryLevelAdjust, - params.octreeElementSizeScale); - - // If we're too far away for our render level, then just return - if (element->distanceToCamera(params.viewFrustum) >= boundaryDistance) { - octreeQueryNode->stats.skippedDistance(element); - params.stopReason = EncodeBitstreamParams::LOD_SKIP; - return bytesAtThisLevel; - } - - // if the parent isn't known to be INSIDE, then it must be INTERSECT, and we should double check to see - // if we are INSIDE, INTERSECT, or OUTSIDE - if (parentLocationThisView != ViewFrustum::INSIDE) { - assert(parentLocationThisView != ViewFrustum::OUTSIDE); // we shouldn't be here if our parent was OUTSIDE! - nodeLocationThisView = element->computeViewIntersection(params.viewFrustum); - } - - // If we're at a element that is out of view, then we can return, because no nodes below us will be in view! - // although technically, we really shouldn't ever be here, because our callers shouldn't be calling us if - // we're out of view - if (nodeLocationThisView == ViewFrustum::OUTSIDE) { - octreeQueryNode->stats.skippedOutOfView(element); - params.stopReason = EncodeBitstreamParams::OUT_OF_VIEW; - return bytesAtThisLevel; - } - - // Ok, we are in view, but if we're in delta mode, then we also want to make sure we weren't already in view - // because we don't send nodes from the previously know in view frustum. - bool wasInView = false; - - if (params.deltaView) { - ViewFrustum::intersection location = element->computeViewIntersection(params.lastViewFrustum); - - // If we're a leaf, then either intersect or inside is considered "formerly in view" - if (element->isLeaf()) { - wasInView = location != ViewFrustum::OUTSIDE; - } else { - wasInView = location == ViewFrustum::INSIDE; - } - - // If we were in view, double check that we didn't switch LOD visibility... namely, the was in view doesn't - // tell us if it was so small we wouldn't have rendered it. Which may be the case. And we may have moved closer - // to it, and so therefore it may now be visible from an LOD perspective, in which case we don't consider it - // as "was in view"... - if (wasInView) { - float boundaryDistance = boundaryDistanceForRenderLevel(element->getLevel() + params.boundaryLevelAdjust, - params.octreeElementSizeScale); - if (element->distanceToCamera(params.lastViewFrustum) >= boundaryDistance) { - // This would have been invisible... but now should be visible (we wouldn't be here otherwise)... - wasInView = false; - } - } - } - - // If we were previously in the view, then we normally will return out of here and stop recursing. But - // if we're in deltaView mode, and this element has changed since it was last sent, then we do - // need to send it. - if (wasInView && !(params.deltaView && element->hasChangedSince(octreeQueryNode->getLastTimeBagEmpty() - CHANGE_FUDGE))) { - octreeQueryNode->stats.skippedWasInView(element); - params.stopReason = EncodeBitstreamParams::WAS_IN_VIEW; - return bytesAtThisLevel; - } - } - - // If we're not in delta sending mode, and we weren't asked to do a force send, and the voxel hasn't changed, - // then we can also bail early and save bits - if (!params.forceSendScene && !params.deltaView && - !element->hasChangedSince(octreeQueryNode->getLastTimeBagEmpty() - CHANGE_FUDGE)) { - - octreeQueryNode->stats.skippedNoChange(element); - - params.stopReason = EncodeBitstreamParams::NO_CHANGE; - return bytesAtThisLevel; - } - - bool keepDiggingDeeper = true; // Assuming we're in view we have a great work ethic, we're always ready for more! - - // At any given point in writing the bitstream, the largest minimum we might need to flesh out the current level - // is 1 byte for child colors + 3*NUMBER_OF_CHILDREN bytes for the actual colors + 1 byte for child trees. - // There could be sub trees below this point, which might take many more bytes, but that's ok, because we can - // always mark our subtrees as not existing and stop the packet at this point, then start up with a new packet - // for the remaining sub trees. - unsigned char childrenExistInTreeBits = 0; - unsigned char childrenExistInPacketBits = 0; - unsigned char childrenDataBits = 0; - - // Make our local buffer large enough to handle writing at this level in case we need to. - LevelDetails thisLevelKey = packetData->startLevel(); - int requiredBytes = sizeof(childrenDataBits) + sizeof(childrenExistInPacketBits); - if (params.includeExistsBits) { - requiredBytes += sizeof(childrenExistInTreeBits); - } - - // If this datatype allows root elements to include data, and this is the root, then ask the tree for the - // minimum bytes needed for root data and reserve those also - if (element == _rootElement && rootElementHasData()) { - requiredBytes += minimumRequiredRootDataBytes(); - } - - bool continueThisLevel = packetData->reserveBytes(requiredBytes); - - // If we can't reserve our minimum bytes then we can discard this level and return as if none of this level fits - if (!continueThisLevel) { - packetData->discardLevel(thisLevelKey); - params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - bag.insert(element); - return bytesAtThisLevel; - } - - int inViewCount = 0; - int inViewNotLeafCount = 0; - int inViewWithColorCount = 0; - - OctreeElementPointer sortedChildren[NUMBER_OF_CHILDREN] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; - float distancesToChildren[NUMBER_OF_CHILDREN] = { 0, 0, 0, 0, 0, 0, 0, 0 }; - int indexOfChildren[NUMBER_OF_CHILDREN] = { 0, 0, 0, 0, 0, 0, 0, 0 }; - - for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { - OctreeElementPointer childElement = element->getChildAtIndex(i); - - // if the caller wants to include childExistsBits, then include them - if (params.includeExistsBits && childElement) { - childrenExistInTreeBits += (1 << (7 - i)); - } - - sortedChildren[i] = childElement; - indexOfChildren[i] = i; - distancesToChildren[i] = 0.0f; - - // track stats - // must check childElement here, because it could be we got here with no childElement - if (childElement) { - octreeQueryNode->stats.traversed(childElement); - } - } - - // for each child element in Distance sorted order..., check to see if they exist, are colored, and in view, and if so - // add them to our distance ordered array of children - for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { - OctreeElementPointer childElement = sortedChildren[i]; - int originalIndex = indexOfChildren[i]; - - bool childIsInView = (childElement && - (params.recurseEverything || !octreeQueryNode->getUsesFrustum() || - (nodeLocationThisView == ViewFrustum::INSIDE) || // parent was fully in view, we can assume ALL children are - (nodeLocationThisView == ViewFrustum::INTERSECT && - childElement->isInView(params.viewFrustum)) // the parent intersects and the child is in view - )); - - if (!childIsInView) { - // must check childElement here, because it could be we got here because there was no childElement - if (childElement) { - octreeQueryNode->stats.skippedOutOfView(childElement); - } - } else { - // Before we consider this further, let's see if it's in our LOD scope... - float boundaryDistance = params.recurseEverything || !octreeQueryNode->getUsesFrustum() ? 1 : - boundaryDistanceForRenderLevel(childElement->getLevel() + params.boundaryLevelAdjust, - params.octreeElementSizeScale); - - if (!(distancesToChildren[i] < boundaryDistance)) { - // don't need to check childElement here, because we can't get here with no childElement - octreeQueryNode->stats.skippedDistance(childElement); - } else { - inViewCount++; - - // track children in view as existing and not a leaf, if they're a leaf, - // we don't care about recursing deeper on them, and we don't consider their - // subtree to exist - if (!(childElement && childElement->isLeaf())) { - childrenExistInPacketBits += (1 << (7 - originalIndex)); - inViewNotLeafCount++; - } - - bool childIsOccluded = false; // assume it's not occluded - - bool shouldRender = params.recurseEverything || !octreeQueryNode->getUsesFrustum() || - childElement->calculateShouldRender(params.viewFrustum, - params.octreeElementSizeScale, params.boundaryLevelAdjust); - - // track some stats - // don't need to check childElement here, because we can't get here with no childElement - if (!shouldRender && childElement->isLeaf()) { - octreeQueryNode->stats.skippedDistance(childElement); - } - // don't need to check childElement here, because we can't get here with no childElement - if (childIsOccluded) { - octreeQueryNode->stats.skippedOccluded(childElement); - } - - // track children with actual color, only if the child wasn't previously in view! - if (shouldRender && !childIsOccluded) { - bool childWasInView = false; - - if (childElement && params.deltaView) { - ViewFrustum::intersection location = childElement->computeViewIntersection(params.lastViewFrustum); - - // If we're a leaf, then either intersect or inside is considered "formerly in view" - if (childElement->isLeaf()) { - childWasInView = location != ViewFrustum::OUTSIDE; - } else { - childWasInView = location == ViewFrustum::INSIDE; - } - } - - // If our child wasn't in view (or we're ignoring wasInView) then we add it to our sending items. - // Or if we were previously in the view, but this element has changed since it was last sent, then we do - // need to send it. - if (!childWasInView || - (params.deltaView && - childElement->hasChangedSince(octreeQueryNode->getLastTimeBagEmpty() - CHANGE_FUDGE))){ - - childrenDataBits += (1 << (7 - originalIndex)); - inViewWithColorCount++; - } else { - // otherwise just track stats of the items we discarded - // don't need to check childElement here, because we can't get here with no childElement - if (childWasInView) { - octreeQueryNode->stats.skippedWasInView(childElement); - } else { - octreeQueryNode->stats.skippedNoChange(childElement); - } - } - } - } - } - } - - // NOTE: the childrenDataBits indicates that there is an array of child element data included in this packet. - // We will write this bit mask but we may come back later and update the bits that are actually included - packetData->releaseReservedBytes(sizeof(childrenDataBits)); - continueThisLevel = packetData->appendBitMask(childrenDataBits); - - int childDataBitsPlaceHolder = packetData->getUncompressedByteOffset(sizeof(childrenDataBits)); - unsigned char actualChildrenDataBits = 0; - - assert(continueThisLevel); // since we used reserved bits, this really shouldn't fail - bytesAtThisLevel += sizeof(childrenDataBits); // keep track of byte count - - octreeQueryNode->stats.colorBitsWritten(); // really data bits not just color bits - - // NOW might be a good time to give our tree subclass and this element a chance to set up and check any extra encode data - element->initializeExtraEncodeData(params); - - // write the child element data... - // NOTE: the format of the bitstream is generally this: - // [octalcode] - // [bitmask for existence of child data] - // N x [child data] - // [bitmask for existence of child elements in tree] - // [bitmask for existence of child elements in buffer] - // N x [ ... tree for children ...] - // - // This section of the code, is writing the "N x [child data]" portion of this bitstream - for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { - if (oneAtBit(childrenDataBits, i)) { - OctreeElementPointer childElement = element->getChildAtIndex(i); - - // the childrenDataBits were set up by the in view/LOD logic, it may contain children that we've already - // processed and sent the data bits for. Let our tree subclass determine if it really wants to send the - // data for this child at this point - if (childElement && element->shouldIncludeChildData(i, params)) { - - int bytesBeforeChild = packetData->getUncompressedSize(); - - // a childElement may "partially" write it's data. for example, the model server where the entire - // contents of the element may be larger than can fit in a single MTU/packetData. In this case, - // we want to allow the appendElementData() to respond that it produced partial data, which should be - // written, but that the childElement needs to be reprocessed in an additional pass or passes - // to be completed. - LevelDetails childDataLevelKey = packetData->startLevel(); - - OctreeElement::AppendState childAppendState = childElement->appendElementData(packetData, params); - - // allow our tree subclass to do any additional bookkeeping it needs to do with encoded data state - element->updateEncodedData(i, childAppendState, params); - - // Continue this level so long as some part of this child element was appended. - bool childFit = (childAppendState != OctreeElement::NONE); - - // some datatypes (like Voxels) assume that all child data will fit, if it doesn't fit - // the data type wants to bail on this element level completely - if (!childFit && mustIncludeAllChildData()) { - continueThisLevel = false; - break; - } - - // If the child was partially or fully appended, then mark the actualChildrenDataBits as including - // this child data - if (childFit) { - actualChildrenDataBits += (1 << (7 - i)); - continueThisLevel = packetData->endLevel(childDataLevelKey); - } else { - packetData->discardLevel(childDataLevelKey); - elementAppendState = OctreeElement::PARTIAL; - params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - } - - // If this child was partially appended, then consider this element to be partially appended - if (childAppendState == OctreeElement::PARTIAL) { - elementAppendState = OctreeElement::PARTIAL; - params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - } - - int bytesAfterChild = packetData->getUncompressedSize(); - - bytesAtThisLevel += (bytesAfterChild - bytesBeforeChild); // keep track of byte count for this child - - // don't need to check childElement here, because we can't get here with no childElement - if (childAppendState != OctreeElement::NONE) { - octreeQueryNode->stats.colorSent(childElement); - } - } - } - } - - if (!mustIncludeAllChildData() && !continueThisLevel) { - qCDebug(octree) << "WARNING UNEXPECTED CASE: reached end of child element data loop with continueThisLevel=FALSE"; - qCDebug(octree) << "This is not expected!!!! -- continueThisLevel=FALSE...."; - } - - if (continueThisLevel && actualChildrenDataBits != childrenDataBits) { - // repair the child data mask - continueThisLevel = packetData->updatePriorBitMask(childDataBitsPlaceHolder, actualChildrenDataBits); - if (!continueThisLevel) { - qCDebug(octree) << "WARNING UNEXPECTED CASE: Failed to update childDataBitsPlaceHolder"; - qCDebug(octree) << "This is not expected!!!! -- continueThisLevel=FALSE...."; - } - } - - // if the caller wants to include childExistsBits, then include them even if not in view, put them before the - // childrenExistInPacketBits, so that the lower code can properly repair the packet exists bits - if (continueThisLevel && params.includeExistsBits) { - packetData->releaseReservedBytes(sizeof(childrenExistInTreeBits)); - continueThisLevel = packetData->appendBitMask(childrenExistInTreeBits); - if (continueThisLevel) { - bytesAtThisLevel += sizeof(childrenExistInTreeBits); // keep track of byte count - - octreeQueryNode->stats.existsBitsWritten(); - } else { - qCDebug(octree) << "WARNING UNEXPECTED CASE: Failed to append childrenExistInTreeBits"; - qCDebug(octree) << "This is not expected!!!! -- continueThisLevel=FALSE...."; - } - } - - // write the child exist bits - if (continueThisLevel) { - packetData->releaseReservedBytes(sizeof(childrenExistInPacketBits)); - continueThisLevel = packetData->appendBitMask(childrenExistInPacketBits); - if (continueThisLevel) { - bytesAtThisLevel += sizeof(childrenExistInPacketBits); // keep track of byte count - - octreeQueryNode->stats.existsInPacketBitsWritten(); - } else { - qCDebug(octree) << "WARNING UNEXPECTED CASE: Failed to append childrenExistInPacketBits"; - qCDebug(octree) << "This is not expected!!!! -- continueThisLevel=FALSE...."; - } - } - - // We only need to keep digging, if there is at least one child that is inView, and not a leaf. - keepDiggingDeeper = (inViewNotLeafCount > 0); - - // - // NOTE: the format of the bitstream is generally this: - // [octalcode] - // [bitmask for existence of child data] - // N x [child data] - // [bitmask for existence of child elements in tree] - // [bitmask for existence of child elements in buffer] - // N x [ ... tree for children ...] - // - // This section of the code, is writing the "N x [ ... tree for children ...]" portion of this bitstream - // - if (continueThisLevel && keepDiggingDeeper) { - - // at this point, we need to iterate the children who are in view, even if not colored - // and we need to determine if there's a deeper tree below them that we care about. - // - // Since this recursive function assumes we're already writing, we know we've already written our - // childrenExistInPacketBits. But... we don't really know how big the child tree will be. And we don't know if - // we'll have room in our buffer to actually write all these child trees. What we kinda would like to do is - // write our childExistsBits as a place holder. Then let each potential tree have a go at it. If they - // write something, we keep them in the bits, if they don't, we take them out. - // - // we know the last thing we wrote to the packet was our childrenExistInPacketBits. Let's remember where that was! - int childExistsPlaceHolder = packetData->getUncompressedByteOffset(sizeof(childrenExistInPacketBits)); - - // we are also going to recurse these child trees in "distance" sorted order, but we need to pack them in the - // final packet in standard order. So what we're going to do is keep track of how big each subtree was in bytes, - // and then later reshuffle these sections of our output buffer back into normal order. This allows us to make - // a single recursive pass in distance sorted order, but retain standard order in our encoded packet - - // for each child element in Distance sorted order..., check to see if they exist, are colored, and in view, and if so - // add them to our distance ordered array of children - for (int indexByDistance = 0; indexByDistance < NUMBER_OF_CHILDREN; indexByDistance++) { - OctreeElementPointer childElement = sortedChildren[indexByDistance]; - int originalIndex = indexOfChildren[indexByDistance]; - - if (oneAtBit(childrenExistInPacketBits, originalIndex)) { - - int thisLevel = currentEncodeLevel; - - int childTreeBytesOut = 0; - - // NOTE: some octree styles (like models and particles) will store content in parent elements, and child - // elements. In this case, if we stop recursion when we include any data (the colorbits should really be - // called databits), then we wouldn't send the children. So those types of Octree's should tell us to keep - // recursing, by returning TRUE in recurseChildrenWithData(). - - if (params.recurseEverything || !octreeQueryNode->getUsesFrustum() - || recurseChildrenWithData() || !oneAtBit(childrenDataBits, originalIndex)) { - - // Allow the datatype a chance to determine if it really wants to recurse this tree. Usually this - // will be true. But if the tree has already been encoded, we will skip this. - if (element->shouldRecurseChildTree(originalIndex, params)) { - childTreeBytesOut = encodeTreeBitstreamRecursion(childElement, packetData, bag, params, - thisLevel, nodeLocationThisView); - } else { - childTreeBytesOut = 0; - } - } - - // if the child wrote 0 bytes, it means that nothing below exists or was in view, or we ran out of space, - // basically, the children below don't contain any info. - - // if the child tree wrote 1 byte??? something must have gone wrong... because it must have at least the color - // byte and the child exist byte. - // - assert(childTreeBytesOut != 1); - - // if the child tree wrote just 2 bytes, then it means: it had no colors and no child nodes, because... - // if it had colors it would write 1 byte for the color mask, - // and at least a color's worth of bytes for the element of colors. - // if it had child trees (with something in them) then it would have the 1 byte for child mask - // and some number of bytes of lower children... - // so, if the child returns 2 bytes out, we can actually consider that an empty tree also!! - // - // we can make this act like no bytes out, by just resetting the bytes out in this case - if (suppressEmptySubtrees() && !params.includeExistsBits && childTreeBytesOut == 2) { - childTreeBytesOut = 0; // this is the degenerate case of a tree with no colors and no child trees - - } - - bytesAtThisLevel += childTreeBytesOut; - - // If we had previously started writing, and if the child DIDN'T write any bytes, - // then we want to remove their bit from the childExistsPlaceHolder bitmask - if (childTreeBytesOut == 0) { - - // remove this child's bit... - childrenExistInPacketBits -= (1 << (7 - originalIndex)); - - // repair the child exists mask - continueThisLevel = packetData->updatePriorBitMask(childExistsPlaceHolder, childrenExistInPacketBits); - if (!continueThisLevel) { - qCDebug(octree) << "WARNING UNEXPECTED CASE: Failed to update childExistsPlaceHolder"; - qCDebug(octree) << "This is not expected!!!! -- continueThisLevel=FALSE...."; - } - - // If this is the last of the child exists bits, then we're actually be rolling out the entire tree - if (childrenExistInPacketBits == 0) { - octreeQueryNode->stats.childBitsRemoved(params.includeExistsBits); - } - - if (!continueThisLevel) { - if (wantDebug) { - qCDebug(octree) << " WARNING line:" << __LINE__; - qCDebug(octree) << " breaking the child recursion loop with continueThisLevel=false!!!"; - qCDebug(octree) << " AFTER attempting to updatePriorBitMask() for empty sub tree...."; - qCDebug(octree) << " IS THIS ACCEPTABLE!!!!"; - } - break; // can't continue... - } - - // Note: no need to move the pointer, cause we already stored this - } // end if (childTreeBytesOut == 0) - } // end if (oneAtBit(childrenExistInPacketBits, originalIndex)) - } // end for - } // end keepDiggingDeeper - - // If we made it this far, then we've written all of our child data... if this element is the root - // element, then we also allow the root element to write out it's data... - if (continueThisLevel && element == _rootElement && rootElementHasData()) { - int bytesBeforeChild = packetData->getUncompressedSize(); - - // release the bytes we reserved... - packetData->releaseReservedBytes(minimumRequiredRootDataBytes()); - - LevelDetails rootDataLevelKey = packetData->startLevel(); - OctreeElement::AppendState rootAppendState = element->appendElementData(packetData, params); - - bool partOfRootFit = (rootAppendState != OctreeElement::NONE); - bool allOfRootFit = (rootAppendState == OctreeElement::COMPLETED); - - if (partOfRootFit) { - continueThisLevel = packetData->endLevel(rootDataLevelKey); - if (!continueThisLevel) { - qCDebug(octree) << " UNEXPECTED ROOT ELEMENT -- could not packetData->endLevel(rootDataLevelKey) -- line:" << __LINE__; - } - } else { - packetData->discardLevel(rootDataLevelKey); - } - - if (!allOfRootFit) { - elementAppendState = OctreeElement::PARTIAL; - params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - } - - // do we really ever NOT want to continue this level??? - //continueThisLevel = (rootAppendState == OctreeElement::COMPLETED); - - int bytesAfterChild = packetData->getUncompressedSize(); - - if (continueThisLevel) { - bytesAtThisLevel += (bytesAfterChild - bytesBeforeChild); // keep track of byte count for this child - - octreeQueryNode->stats.colorSent(element); - } - - if (!continueThisLevel) { - qCDebug(octree) << "WARNING UNEXPECTED CASE: Something failed in packing ROOT data"; - qCDebug(octree) << "This is not expected!!!! -- continueThisLevel=FALSE...."; - } - } - - // if we were unable to fit this level in our packet, then rewind and add it to the element bag for - // sending later... - if (continueThisLevel) { - continueThisLevel = packetData->endLevel(thisLevelKey); - } else { - packetData->discardLevel(thisLevelKey); - - if (!mustIncludeAllChildData()) { - qCDebug(octree) << "WARNING UNEXPECTED CASE: Something failed in attempting to pack this element"; - qCDebug(octree) << "This is not expected!!!! -- continueThisLevel=FALSE...."; - } - } - - // This happens if the element could not be written at all. In the case of Octree's that support partial - // element data, continueThisLevel will be true. So this only happens if the full element needs to be - // added back to the element bag. - if (!continueThisLevel) { - if (!mustIncludeAllChildData()) { - qCDebug(octree) << "WARNING UNEXPECTED CASE - Something failed in attempting to pack this element."; - qCDebug(octree) << " If the datatype requires all child data, then this might happen. Otherwise" ; - qCDebug(octree) << " this is an unexpected case and we should research a potential logic error." ; - } - - bag.insert(element); - - // don't need to check element here, because we can't get here with no element - octreeQueryNode->stats.didntFit(element); - - params.stopReason = EncodeBitstreamParams::DIDNT_FIT; - bytesAtThisLevel = 0; // didn't fit - } else { - - // assuming we made it here with continueThisLevel == true, we STILL might want - // to add our element back to the bag for additional encoding, specifically if - // the appendState is PARTIAL, in this case, we re-add our element to the bag - // and assume that the appendElementData() has stored any required state data - // in the params extraEncodeData - if (elementAppendState == OctreeElement::PARTIAL) { - bag.insert(element); - } - } - - // If our element is completed let the element know so it can do any cleanup it of extra wants - if (elementAppendState == OctreeElement::COMPLETED) { - element->elementEncodeComplete(params); - } - - return bytesAtThisLevel; -} - bool Octree::readFromFile(const char* fileName) { QString qFileName = findMostRecentFileExtension(fileName, PERSIST_EXTENSIONS); @@ -1615,14 +774,10 @@ bool Octree::readFromFile(const char* fileName) { QFileInfo fileInfo(qFileName); uint64_t fileLength = fileInfo.size(); - emit importSize(1.0f, 1.0f, 1.0f); - emit importProgress(0); - qCDebug(octree) << "Loading file" << qFileName << "..."; bool success = readFromStream(fileLength, fileInputStream); - emit importProgress(100); file.close(); return success; @@ -1863,7 +1018,3 @@ bool Octree::countOctreeElementsOperation(const OctreeElementPointer& element, v (*(uint64_t*)extraData)++; return true; // keep going } - -void Octree::cancelImport() { - _stopImport = true; -} diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index c4c4508138..a2ad834e18 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -49,126 +49,62 @@ public: // Callback function, for recuseTreeWithOperation using RecurseOctreeOperation = std::function; -typedef enum {GRADIENT, RANDOM, NATURAL} creationMode; typedef QHash CubeList; const bool NO_EXISTS_BITS = false; const bool WANT_EXISTS_BITS = true; -const bool COLLAPSE_EMPTY_TREE = true; -const bool DONT_COLLAPSE = false; -const int DONT_CHOP = 0; const int NO_BOUNDARY_ADJUST = 0; const int LOW_RES_MOVING_ADJUST = 1; -#define IGNORE_COVERAGE_MAP NULL - class EncodeBitstreamParams { public: ViewFrustum viewFrustum; - ViewFrustum lastViewFrustum; - int maxEncodeLevel; - int maxLevelReached; bool includeExistsBits; - int chopLevels; - bool deltaView; - bool recurseEverything { false }; - int boundaryLevelAdjust; - float octreeElementSizeScale; - bool forceSendScene; NodeData* nodeData; // output hints from the encode process typedef enum { UNKNOWN, DIDNT_FIT, - NULL_NODE, - NULL_NODE_DATA, - TOO_DEEP, - LOD_SKIP, - OUT_OF_VIEW, - WAS_IN_VIEW, - NO_CHANGE, - OCCLUDED, FINISHED } reason; reason stopReason; - EncodeBitstreamParams( - int maxEncodeLevel = INT_MAX, - bool includeExistsBits = WANT_EXISTS_BITS, - int chopLevels = 0, - bool useDeltaView = false, - int boundaryLevelAdjust = NO_BOUNDARY_ADJUST, - float octreeElementSizeScale = DEFAULT_OCTREE_SIZE_SCALE, - bool forceSendScene = true, - NodeData* nodeData = nullptr) : - maxEncodeLevel(maxEncodeLevel), - maxLevelReached(0), + EncodeBitstreamParams(bool includeExistsBits = WANT_EXISTS_BITS, + NodeData* nodeData = nullptr) : includeExistsBits(includeExistsBits), - chopLevels(chopLevels), - deltaView(useDeltaView), - boundaryLevelAdjust(boundaryLevelAdjust), - octreeElementSizeScale(octreeElementSizeScale), - forceSendScene(forceSendScene), nodeData(nodeData), stopReason(UNKNOWN) { - lastViewFrustum.invalidate(); } void displayStopReason() { printf("StopReason: "); switch (stopReason) { - default: case UNKNOWN: qDebug("UNKNOWN"); break; - case DIDNT_FIT: qDebug("DIDNT_FIT"); break; - case NULL_NODE: qDebug("NULL_NODE"); break; - case TOO_DEEP: qDebug("TOO_DEEP"); break; - case LOD_SKIP: qDebug("LOD_SKIP"); break; - case OUT_OF_VIEW: qDebug("OUT_OF_VIEW"); break; - case WAS_IN_VIEW: qDebug("WAS_IN_VIEW"); break; - case NO_CHANGE: qDebug("NO_CHANGE"); break; - case OCCLUDED: qDebug("OCCLUDED"); break; + case FINISHED: qDebug("FINISHED"); break; } } QString getStopReason() { switch (stopReason) { - default: case UNKNOWN: return QString("UNKNOWN"); break; - case DIDNT_FIT: return QString("DIDNT_FIT"); break; - case NULL_NODE: return QString("NULL_NODE"); break; - case TOO_DEEP: return QString("TOO_DEEP"); break; - case LOD_SKIP: return QString("LOD_SKIP"); break; - case OUT_OF_VIEW: return QString("OUT_OF_VIEW"); break; - case WAS_IN_VIEW: return QString("WAS_IN_VIEW"); break; - case NO_CHANGE: return QString("NO_CHANGE"); break; - case OCCLUDED: return QString("OCCLUDED"); break; + case FINISHED: return QString("FINISHED"); break; } } std::function trackSend { [](const QUuid&, quint64){} }; }; -class ReadElementBufferToTreeArgs { -public: - const unsigned char* buffer; - int length; - bool destructive; - bool pathChanged; -}; - class ReadBitstreamToTreeParams { public: bool includeExistsBits; OctreeElementPointer destinationElement; QUuid sourceUUID; SharedNodePointer sourceNode; - bool wantImportProgress; - PacketVersion bitstreamVersion; int elementsPerPacket = 0; int entitiesPerPacket = 0; @@ -176,15 +112,11 @@ public: bool includeExistsBits = WANT_EXISTS_BITS, OctreeElementPointer destinationElement = NULL, QUuid sourceUUID = QUuid(), - SharedNodePointer sourceNode = SharedNodePointer(), - bool wantImportProgress = false, - PacketVersion bitstreamVersion = 0) : + SharedNodePointer sourceNode = SharedNodePointer()) : includeExistsBits(includeExistsBits), destinationElement(destinationElement), sourceUUID(sourceUUID), - sourceNode(sourceNode), - wantImportProgress(wantImportProgress), - bitstreamVersion(bitstreamVersion) + sourceNode(sourceNode) {} }; @@ -199,7 +131,6 @@ public: // These methods will allow the OctreeServer to send your tree inbound edit packets of your // own definition. Implement these to allow your octree based server to support editing - virtual bool getWantSVOfileVersions() const { return false; } virtual PacketType expectedDataPacketType() const { return PacketType::Unknown; } virtual PacketVersion expectedVersion() const { return versionForPacketType(expectedDataPacketType()); } virtual bool handlesEditPacketType(PacketType packetType) const { return false; } @@ -209,12 +140,8 @@ public: virtual void processChallengeOwnershipReplyPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { return; } virtual void processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) { return; } - virtual bool recurseChildrenWithData() const { return true; } virtual bool rootElementHasData() const { return false; } - virtual int minimumRequiredRootDataBytes() const { return 0; } - virtual bool suppressEmptySubtrees() const { return true; } virtual void releaseSceneEncodeData(OctreeElementExtraEncodeData* extraEncodeData) const { } - virtual bool mustIncludeAllChildData() const { return true; } virtual void update() { } // nothing to do by default @@ -223,11 +150,8 @@ public: virtual void eraseAllOctreeElements(bool createNewRoot = true); virtual void readBitstreamToTree(const unsigned char* bitstream, uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args); - void deleteOctalCodeFromTree(const unsigned char* codeBuffer, bool collapseEmptyTrees = DONT_COLLAPSE); void reaverageOctreeElements(OctreeElementPointer startElement = OctreeElementPointer()); - void deleteOctreeElementAt(float x, float y, float z, float s); - /// Find the voxel at position x,y,z,s /// \return pointer to the OctreeElement or NULL if none at x,y,z,s. OctreeElementPointer getOctreeElementAt(float x, float y, float z, float s) const; @@ -250,8 +174,6 @@ public: void recurseTreeWithOperator(RecurseOctreeOperator* operatorObject); - int encodeTreeBitstream(const OctreeElementPointer& element, OctreePacketData* packetData, OctreeElementBag& bag, - EncodeBitstreamParams& params) ; bool isDirty() const { return _isDirty; } void clearDirtyBit() { _isDirty = false; } @@ -344,22 +266,10 @@ public: void incrementPersistDataVersion() { _persistDataVersion++; } -signals: - void importSize(float x, float y, float z); - void importProgress(int progress); - -public slots: - void cancelImport(); - protected: void deleteOctalCodeFromTreeRecursion(const OctreeElementPointer& element, void* extraData); - int encodeTreeBitstreamRecursion(const OctreeElementPointer& element, - OctreePacketData* packetData, OctreeElementBag& bag, - EncodeBitstreamParams& params, int& currentEncodeLevel, - const ViewFrustum::intersection& parentLocationThisView) const; - static bool countOctreeElementsOperation(const OctreeElementPointer& element, void* extraData); OctreeElementPointer nodeForOctalCode(const OctreeElementPointer& ancestorElement, const unsigned char* needleCode, OctreeElementPointer* parentOfFoundElement) const; @@ -374,7 +284,6 @@ protected: bool _isDirty; bool _shouldReaverage; - bool _stopImport; bool _isViewing; bool _isServer; diff --git a/libraries/octree/src/OctreeElement.cpp b/libraries/octree/src/OctreeElement.cpp index 989951b661..6446e3b460 100644 --- a/libraries/octree/src/OctreeElement.cpp +++ b/libraries/octree/src/OctreeElement.cpp @@ -461,33 +461,6 @@ ViewFrustum::intersection OctreeElement::computeViewIntersection(const ViewFrust return viewFrustum.calculateCubeKeyholeIntersection(_cube); } -// There are two types of nodes for which we want to "render" -// 1) Leaves that are in the LOD -// 2) Non-leaves are more complicated though... usually you don't want to render them, but if their children -// wouldn't be rendered, then you do want to render them. But sometimes they have some children that ARE -// in the LOD, and others that are not. In this case we want to render the parent, and none of the children. -// -// Since, if we know the camera position and orientation, we can know which of the corners is the "furthest" -// corner. We can use we can use this corner as our "voxel position" to do our distance calculations off of. -// By doing this, we don't need to test each child voxel's position vs the LOD boundary -bool OctreeElement::calculateShouldRender(const ViewFrustum& viewFrustum, float voxelScaleSize, int boundaryLevelAdjust) const { - bool shouldRender = false; - - if (hasContent()) { - float furthestDistance = furthestDistanceToCamera(viewFrustum); - float childBoundary = boundaryDistanceForRenderLevel(getLevel() + 1 + boundaryLevelAdjust, voxelScaleSize); - bool inChildBoundary = (furthestDistance <= childBoundary); - if (hasDetailedContent() && inChildBoundary) { - shouldRender = true; - } else { - float boundary = childBoundary * 2.0f; // the boundary is always twice the distance of the child boundary - bool inBoundary = (furthestDistance <= boundary); - shouldRender = inBoundary && !inChildBoundary; - } - } - return shouldRender; -} - // Calculates the distance to the furthest point of the voxel to the camera // does as much math as possible in voxel scale and then scales up to TREE_SCALE at end float OctreeElement::furthestDistanceToCamera(const ViewFrustum& viewFrustum) const { diff --git a/libraries/octree/src/OctreeElement.h b/libraries/octree/src/OctreeElement.h index 514039713b..b7857c3e6c 100644 --- a/libraries/octree/src/OctreeElement.h +++ b/libraries/octree/src/OctreeElement.h @@ -85,16 +85,6 @@ public: typedef enum { COMPLETED, PARTIAL, NONE } AppendState; virtual void debugExtraEncodeData(EncodeBitstreamParams& params) const { } - virtual void initializeExtraEncodeData(EncodeBitstreamParams& params) { } - virtual bool shouldIncludeChildData(int childIndex, EncodeBitstreamParams& params) const { return true; } - virtual bool shouldRecurseChildTree(int childIndex, EncodeBitstreamParams& params) const { return true; } - - virtual void updateEncodedData(int childIndex, AppendState childAppendState, EncodeBitstreamParams& params) const { } - virtual void elementEncodeComplete(EncodeBitstreamParams& params) const { } - - /// Override to serialize the state of this element. This is used for persistance and for transmission across the network. - virtual AppendState appendElementData(OctreePacketData* packetData, EncodeBitstreamParams& params) const - { return COMPLETED; } /// Override to deserialize the state of this element. This is used for loading from a persisted file or from reading /// from the network. @@ -139,9 +129,6 @@ public: float distanceToCamera(const ViewFrustum& viewFrustum) const; float furthestDistanceToCamera(const ViewFrustum& viewFrustum) const; - bool calculateShouldRender(const ViewFrustum& viewFrustum, - float voxelSizeScale = DEFAULT_OCTREE_SIZE_SCALE, int boundaryLevelAdjust = 0) const; - // points are assumed to be in Voxel Coordinates (not TREE_SCALE'd) float distanceSquareToPoint(const glm::vec3& point) const; // when you don't need the actual distance, use this. float distanceToPoint(const glm::vec3& point) const; diff --git a/libraries/octree/src/OctreeElementBag.cpp b/libraries/octree/src/OctreeElementBag.cpp deleted file mode 100644 index afd2d5cdc3..0000000000 --- a/libraries/octree/src/OctreeElementBag.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// -// OctreeElementBag.cpp -// libraries/octree/src -// -// Created by Brad Hefta-Gaub on 4/25/2013. -// Copyright 2013 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 -// - -#include "OctreeElementBag.h" -#include - -void OctreeElementBag::deleteAll() { - _bagElements = Bag(); -} - -/// does the bag contain elements? -/// if all of the contained elements are expired, they will not report as empty, and -/// a single last item will be returned by extract as a null pointer -bool OctreeElementBag::isEmpty() { - return _bagElements.empty(); -} - -void OctreeElementBag::insert(const OctreeElementPointer& element) { - _bagElements[element.get()] = element; -} - -OctreeElementPointer OctreeElementBag::extract() { - OctreeElementPointer result; - - // Find the first element still alive - Bag::iterator it = _bagElements.begin(); - while (it != _bagElements.end() && !result) { - result = it->second.lock(); - it = _bagElements.erase(it); - } - return result; -} diff --git a/libraries/octree/src/OctreeElementBag.h b/libraries/octree/src/OctreeElementBag.h index 34c49f1e60..a79648f596 100644 --- a/libraries/octree/src/OctreeElementBag.h +++ b/libraries/octree/src/OctreeElementBag.h @@ -16,30 +16,8 @@ #ifndef hifi_OctreeElementBag_h #define hifi_OctreeElementBag_h -#include - #include "OctreeElement.h" -class OctreeElementBag { - using Bag = std::unordered_map; - -public: - void insert(const OctreeElementPointer& element); // put a element into the bag - - OctreeElementPointer extract(); /// pull a element out of the bag (could come in any order) and if all of the - /// elements have expired, a single null pointer will be returned - - bool isEmpty(); /// does the bag contain elements, - /// if all of the contained elements are expired, they will not report as empty, and - /// a single last item will be returned by extract as a null pointer - - void deleteAll(); - size_t size() const { return _bagElements.size(); } - -private: - Bag _bagElements; -}; - class OctreeElementExtraEncodeDataBase { public: OctreeElementExtraEncodeDataBase() {} diff --git a/libraries/octree/src/OctreeProcessor.cpp b/libraries/octree/src/OctreeProcessor.cpp index 65b30dd197..0808e817ed 100644 --- a/libraries/octree/src/OctreeProcessor.cpp +++ b/libraries/octree/src/OctreeProcessor.cpp @@ -117,7 +117,7 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe if (sectionLength) { // ask the VoxelTree to read the bitstream into the tree ReadBitstreamToTreeParams args(WANT_EXISTS_BITS, NULL, - sourceUUID, sourceNode, false, message.getVersion()); + sourceUUID, sourceNode); quint64 startUncompress, startLock = usecTimestampNow(); quint64 startReadBitsteam, endReadBitsteam; // FIXME STUTTER - there may be an opportunity to bump this lock outside of the diff --git a/libraries/octree/src/OctreeQueryNode.cpp b/libraries/octree/src/OctreeQueryNode.cpp index f0c9027493..16542b697e 100644 --- a/libraries/octree/src/OctreeQueryNode.cpp +++ b/libraries/octree/src/OctreeQueryNode.cpp @@ -144,11 +144,6 @@ void OctreeQueryNode::copyCurrentViewFrustum(ViewFrustum& viewOut) const { viewOut = _currentViewFrustum; } -void OctreeQueryNode::copyLastKnownViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _lastKnownViewFrustum; -} - bool OctreeQueryNode::updateCurrentViewFrustum() { // if shutting down, return immediately if (_isShuttingDown) { @@ -229,70 +224,6 @@ void OctreeQueryNode::setViewSent(bool viewSent) { } } -void OctreeQueryNode::updateLastKnownViewFrustum() { - // if shutting down, return immediately - if (_isShuttingDown) { - return; - } - - { - QMutexLocker viewLocker(&_viewMutex); - bool frustumChanges = !_lastKnownViewFrustum.isVerySimilar(_currentViewFrustum); - - if (frustumChanges) { - // save our currentViewFrustum into our lastKnownViewFrustum - _lastKnownViewFrustum = _currentViewFrustum; - } - } - - // save that we know the view has been sent. - setLastTimeBagEmpty(); -} - - -bool OctreeQueryNode::moveShouldDump() const { - // if shutting down, return immediately - if (_isShuttingDown) { - return false; - } - - QMutexLocker viewLocker(&_viewMutex); - glm::vec3 oldPosition = _lastKnownViewFrustum.getPosition(); - glm::vec3 newPosition = _currentViewFrustum.getPosition(); - - // theoretically we could make this slightly larger but relative to avatar scale. - const float MAXIMUM_MOVE_WITHOUT_DUMP = 0.0f; - return glm::distance(newPosition, oldPosition) > MAXIMUM_MOVE_WITHOUT_DUMP; -} - -void OctreeQueryNode::dumpOutOfView() { - // if shutting down, return immediately - if (_isShuttingDown) { - return; - } - - int stillInView = 0; - int outOfView = 0; - OctreeElementBag tempBag; - ViewFrustum viewCopy; - copyCurrentViewFrustum(viewCopy); - while (OctreeElementPointer elementToCheck = elementBag.extract()) { - if (elementToCheck->isInView(viewCopy)) { - tempBag.insert(elementToCheck); - stillInView++; - } else { - outOfView++; - } - } - if (stillInView > 0) { - while (OctreeElementPointer elementToKeepInBag = tempBag.extract()) { - if (elementToKeepInBag->isInView(viewCopy)) { - elementBag.insert(elementToKeepInBag); - } - } - } -} - void OctreeQueryNode::packetSent(const NLPacket& packet) { _sentPacketHistory.packetSent(_sequenceNumber, packet); _sequenceNumber++; diff --git a/libraries/octree/src/OctreeQueryNode.h b/libraries/octree/src/OctreeQueryNode.h index fd89a89949..640a7c7ddc 100644 --- a/libraries/octree/src/OctreeQueryNode.h +++ b/libraries/octree/src/OctreeQueryNode.h @@ -46,23 +46,14 @@ public: bool shouldSuppressDuplicatePacket(); unsigned int getAvailable() const { return _octreePacket->bytesAvailableForWrite(); } - int getMaxSearchLevel() const { return _maxSearchLevel; } - void resetMaxSearchLevel() { _maxSearchLevel = 1; } - void incrementMaxSearchLevel() { _maxSearchLevel++; } - int getMaxLevelReached() const { return _maxLevelReachedInLastSearch; } - void setMaxLevelReached(int maxLevelReached) { _maxLevelReachedInLastSearch = maxLevelReached; } - - OctreeElementBag elementBag; OctreeElementExtraEncodeData extraEncodeData; void copyCurrentViewFrustum(ViewFrustum& viewOut) const; - void copyLastKnownViewFrustum(ViewFrustum& viewOut) const; // These are not classic setters because they are calculating and maintaining state // which is set asynchronously through the network receive bool updateCurrentViewFrustum(); - void updateLastKnownViewFrustum(); bool getViewSent() const { return _viewSent; } void setViewSent(bool viewSent); @@ -70,24 +61,13 @@ public: bool getViewFrustumChanging() const { return _viewFrustumChanging; } bool getViewFrustumJustStoppedChanging() const { return _viewFrustumJustStoppedChanging; } - bool moveShouldDump() const; - - quint64 getLastTimeBagEmpty() const { return _lastTimeBagEmpty; } - void setLastTimeBagEmpty() { _lastTimeBagEmpty = _sceneSendStartTime; } - bool hasLodChanged() const { return _lodChanged; } OctreeSceneStats stats; - void dumpOutOfView(); - - quint64 getLastRootTimestamp() const { return _lastRootTimestamp; } - void setLastRootTimestamp(quint64 timestamp) { _lastRootTimestamp = timestamp; } unsigned int getlastOctreePacketLength() const { return _lastOctreePacketLength; } int getDuplicatePacketCount() const { return _duplicatePacketCount; } - void sceneStart(quint64 sceneSendStartTime) { _sceneSendStartTime = sceneSendStartTime; } - void nodeKilled(); bool isShuttingDown() const { return _isShuttingDown; } @@ -118,18 +98,11 @@ private: int _duplicatePacketCount { 0 }; quint64 _firstSuppressedPacket { usecTimestampNow() }; - int _maxSearchLevel { 1 }; - int _maxLevelReachedInLastSearch { 1 }; - mutable QMutex _viewMutex { QMutex::Recursive }; ViewFrustum _currentViewFrustum; - ViewFrustum _lastKnownViewFrustum; - quint64 _lastTimeBagEmpty { 0 }; bool _viewFrustumChanging { false }; bool _viewFrustumJustStoppedChanging { true }; - OctreeSendThread* _octreeSendThread { nullptr }; - // watch for LOD changes int _lastClientBoundaryLevelAdjust { 0 }; float _lastClientOctreeSizeScale { DEFAULT_OCTREE_SIZE_SCALE }; @@ -138,16 +111,12 @@ private: OCTREE_PACKET_SEQUENCE _sequenceNumber { 0 }; - quint64 _lastRootTimestamp { 0 }; - PacketType _myPacketType { PacketType::Unknown }; bool _isShuttingDown { false }; SentPacketHistory _sentPacketHistory; QQueue _nackedSequenceNumbers; - quint64 _sceneSendStartTime = 0; - std::array _lastOctreePayload; QJsonObject _lastCheckJSONParameters; diff --git a/libraries/octree/src/OctreeSceneStats.cpp b/libraries/octree/src/OctreeSceneStats.cpp index 054603440d..c4d5e4a915 100644 --- a/libraries/octree/src/OctreeSceneStats.cpp +++ b/libraries/octree/src/OctreeSceneStats.cpp @@ -284,10 +284,6 @@ void OctreeSceneStats::didntFit(const OctreeElementPointer& element) { } } -void OctreeSceneStats::colorBitsWritten() { - _colorBitsWritten++; -} - void OctreeSceneStats::existsBitsWritten() { _existsBitsWritten++; } diff --git a/libraries/octree/src/OctreeSceneStats.h b/libraries/octree/src/OctreeSceneStats.h index 78b4dfd26f..e1228609a6 100644 --- a/libraries/octree/src/OctreeSceneStats.h +++ b/libraries/octree/src/OctreeSceneStats.h @@ -79,9 +79,6 @@ public: /// Track that a element was due to be sent, but didn't fit in the packet and was moved to next packet void didntFit(const OctreeElementPointer& element); - /// Track that the color bitmask was was sent as part of computation of a scene - void colorBitsWritten(); - /// Track that the exists in tree bitmask was was sent as part of computation of a scene void existsBitsWritten(); diff --git a/libraries/shared/src/OctalCode.cpp b/libraries/shared/src/OctalCode.cpp index ae4338be6f..b1ceab4149 100644 --- a/libraries/shared/src/OctalCode.cpp +++ b/libraries/shared/src/OctalCode.cpp @@ -248,22 +248,6 @@ void setOctalCodeSectionValue(unsigned char* octalCode, int section, char sectio } } -unsigned char* chopOctalCode(const unsigned char* originalOctalCode, int chopLevels) { - int codeLength = numberOfThreeBitSectionsInCode(originalOctalCode); - unsigned char* newCode = NULL; - if (codeLength > chopLevels) { - int newLength = codeLength - chopLevels; - newCode = new unsigned char[newLength+1]; - *newCode = newLength; // set the length byte - - for (int section = chopLevels; section < codeLength; section++) { - char sectionValue = getOctalCodeSectionValue(originalOctalCode, section); - setOctalCodeSectionValue(newCode, section - chopLevels, sectionValue); - } - } - return newCode; -} - bool isAncestorOf(const unsigned char* possibleAncestor, const unsigned char* possibleDescendent, int descendentsChild) { if (!possibleAncestor || !possibleDescendent) { return false; diff --git a/libraries/shared/src/OctalCode.h b/libraries/shared/src/OctalCode.h index 89c5e6d74e..63cbc58cfa 100644 --- a/libraries/shared/src/OctalCode.h +++ b/libraries/shared/src/OctalCode.h @@ -40,8 +40,6 @@ const int UNKNOWN_OCTCODE_LENGTH = -2; /// \param int maxBytes number of bytes that octalCode is expected to be, -1 if unknown int numberOfThreeBitSectionsInCode(const unsigned char* octalCode, int maxBytes = UNKNOWN_OCTCODE_LENGTH); -unsigned char* chopOctalCode(const unsigned char* originalOctalCode, int chopLevels); - const int CHECK_NODE_ONLY = -1; bool isAncestorOf(const unsigned char* possibleAncestor, const unsigned char* possibleDescendent, int descendentsChild = CHECK_NODE_ONLY); diff --git a/tests/entities/src/main.cpp b/tests/entities/src/main.cpp index bf79f9d3e9..43a5d2e48d 100644 --- a/tests/entities/src/main.cpp +++ b/tests/entities/src/main.cpp @@ -160,7 +160,6 @@ int main(int argc, char** argv) { QByteArray packet = file.readAll(); EntityItemPointer item = ShapeEntityItem::boxFactory(EntityItemID(), EntityItemProperties()); ReadBitstreamToTreeParams params; - params.bitstreamVersion = 33; auto start = usecTimestampNow(); for (int i = 0; i < 1000; ++i) { From a65f5f7917a21eeaffa7cacd230363a12afdf683 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 18 Apr 2018 13:06:26 -0700 Subject: [PATCH 40/48] making tablet changes --- .../tablet-with-home-button-small-bezel.fbx | Bin 0 -> 190780 bytes .../resources/qml/hifi/tablet/TabletMenu.qml | 4 +- .../qml/hifi/tablet/TabletMenuStack.qml | 2 + .../qml/hifi/tablet/TabletMenuView.qml | 8 +-- scripts/system/libraries/WebTablet.js | 48 +++++++++++------- scripts/system/libraries/utils.js | 14 ++--- 6 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 interface/resources/meshes/tablet-with-home-button-small-bezel.fbx diff --git a/interface/resources/meshes/tablet-with-home-button-small-bezel.fbx b/interface/resources/meshes/tablet-with-home-button-small-bezel.fbx new file mode 100644 index 0000000000000000000000000000000000000000..d94ce304298b3f7085b495e5802bc4af613b5ac1 GIT binary patch literal 190780 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�wa#=&oPL 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

Co_i<#vns6( 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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index 457fe84c3a..57c1163eb8 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -14,8 +14,8 @@ FocusScope { id: tabletMenu objectName: "tabletMenu" - width: 480 - height: 720 + width: parent.width + height: parent.height property var rootMenu: Menu { objectName:"rootMenu" } property var point: Qt.point(50, 50); diff --git a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml index 4fa29de9a3..4d36a57793 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -16,6 +16,8 @@ import "." Item { id: root anchors.fill: parent + width: parent.width + height: parent.height objectName: "tabletMenuHandlerItem" StackView { diff --git a/interface/resources/qml/hifi/tablet/TabletMenuView.qml b/interface/resources/qml/hifi/tablet/TabletMenuView.qml index 2d4d31b9aa..b632a17e57 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuView.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuView.qml @@ -40,10 +40,10 @@ FocusScope { id: listView x: 0 y: 0 - width: 480 - height: 720 - contentWidth: 480 - contentHeight: 720 + width: parent.width + height: parent.height + contentWidth: parent.width + contentHeight: parent.height objectName: "menuList" topMargin: hifi.dimensions.menuPadding.y diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index a7186f55bd..783b91f5f0 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -15,7 +15,7 @@ Script.include(Script.resolvePath("../libraries/controllers.js")); Script.include(Script.resolvePath("../libraries/Xform.js")); var Y_AXIS = {x: 0, y: 1, z: 0}; -var DEFAULT_DPI = 34; +var DEFAULT_DPI = 31; var DEFAULT_WIDTH = 0.4375; var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees var SENSOR_TO_ROOM_MATRIX = -2; @@ -31,12 +31,12 @@ var DELAY_FOR_30HZ = 33; // milliseconds // will need to be recaclulated if dimensions of fbx model change. -var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269}; +var TABLET_NATURAL_DIMENSIONS = {x: 32.083, y: 48.553, z: 2.269}; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; // var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx"; -var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx"; +var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button-small-bezel.fbx"; // returns object with two fields: // * position - position in front of the user @@ -44,14 +44,28 @@ var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home- function calcSpawnInfo(hand, landscape) { var finalPosition; + var LEFT_HAND = Controller.Standard.LeftHand; + var sensorToWorldScale = MyAvatar.sensorToWorldScale; var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position; - var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation; - var dominantHandRotation = MyAvatar.getDominantHand() === "right" ? -20 : 20; - var offsetRotation = Quat.fromPitchYawRollDegrees(0, dominantHandRotation, 0); - var forward = Vec3.multiplyQbyV(offsetRotation, Quat.getForward(Quat.cancelOutRollAndPitch(headRot))); - var FORWARD_OFFSET = 0.5 * MyAvatar.sensorToWorldScale; - finalPosition = Vec3.sum(headPos, Vec3.multiply(FORWARD_OFFSET, forward)); - var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.UNIT_Y)); + var headRot = Quat.cancelOutRollAndPitch((HMD.active && Camera.mode === "first person") ? + HMD.orientation : Camera.orientation); + + var right = Quat.getRight(headRot); + var forward = Quat.getForward(headRot); + var up = Quat.getUp(headRot); + + var FORWARD_OFFSET = 0.5 * sensorToWorldScale; + var UP_OFFSET = -0.16 * sensorToWorldScale; + var RIGHT_OFFSET = ((hand === LEFT_HAND) ? -0.18 : 0.18) * sensorToWorldScale; + + var forwardPosition = Vec3.sum(headPos, Vec3.multiply(FORWARD_OFFSET, forward)); + var lateralPosition = Vec3.sum(forwardPosition, Vec3.multiply(RIGHT_OFFSET, right)); + finalPosition = Vec3.sum(lateralPosition, Vec3.multiply(UP_OFFSET, up)); + + var MY_EYES = { x: 0.0, y: 0.15, z: 0.0 }; + var lookAtEndPosition = Vec3.sum(Vec3.multiply(RIGHT_OFFSET, right), Vec3.multiply(FORWARD_OFFSET, forward)); + var orientation = Quat.lookAt(MY_EYES, lookAtEndPosition, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.UNIT_Y)); + return { position: finalPosition, rotation: landscape ? Quat.multiply(orientation, ROT_LANDSCAPE) : Quat.multiply(orientation, ROT_Y_180) @@ -119,11 +133,11 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { Overlays.deleteOverlay(this.webOverlayID); } - var RAYPICK_OFFSET = 0.0001; // Sufficient for raypick to reliably intersect tablet screen before tablet model. + var RAYPICK_OFFSET = 0.0007; // Sufficient for raypick to reliably intersect tablet screen before tablet model. var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) / sensorScaleFactor + RAYPICK_OFFSET; - var WEB_ENTITY_Y_OFFSET = 0.004; - var screenWidth = 0.82 * tabletWidth; - var screenHeight = 0.81 * tabletHeight; + var WEB_ENTITY_Y_OFFSET = 1 * tabletScaleFactor; + var screenWidth = 0.9275 * tabletWidth; + var screenHeight = 0.8983 * tabletHeight; this.webOverlayID = Overlays.addOverlay("web3d", { name: "WebTablet Web", url: url, @@ -139,7 +153,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { visible: visible }); - var HOME_BUTTON_Y_OFFSET = ((tabletHeight / 2) - (tabletHeight / 20)) * (1 / sensorScaleFactor) - 0.003; + var HOME_BUTTON_Y_OFFSET = (tabletHeight / 2) - (tabletHeight / 20) + 0.003 * sensorScaleFactor; // FIXME: Circle3D overlays currently at the wrong dimensions, so we need to account for that here var homeButtonDim = 4.0 * tabletScaleFactor / 3.0; this.homeButtonID = Overlays.addOverlay("circle3d", { @@ -277,8 +291,8 @@ WebTablet.prototype.setLandscape = function(newLandscapeValue) { var tabletWidth = getTabletWidthFromSettings() * MyAvatar.sensorToWorldScale; var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x; var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor; - var screenWidth = 0.82 * tabletWidth; - var screenHeight = 0.81 * tabletHeight; + var screenWidth = 0.9275 * tabletWidth; + var screenHeight = 0.8983 * tabletHeight; Overlays.editOverlay(this.webOverlayID, { rotation: Quat.multiply(cameraOrientation, ROT_LANDSCAPE_WINDOW), dimensions: {x: this.landscape ? screenHeight : screenWidth, y: this.landscape ? screenWidth : screenHeight, z: 0.1} diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 6afde85c29..7e9e1d7e6a 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -373,7 +373,6 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) if (!HMD.tabletID || !HMD.tabletScreenID || !HMD.homeButtonID || !HMD.homeButtonHighlightID) { return; } - var sensorScaleFactor = sensorToWorldScaleOverride || MyAvatar.sensorToWorldScale; var sensorScaleOffsetOverride = 1; var SENSOR_TO_ROOM_MATRIX = 65534; @@ -383,8 +382,8 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) } // will need to be recaclulated if dimensions of fbx model change. - var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269}; - var DEFAULT_DPI = 34; + var TABLET_NATURAL_DIMENSIONS = {x: 32.083, y: 48.553, z: 2.269}; + var DEFAULT_DPI = 31; var DEFAULT_WIDTH = 0.4375; // scale factor of natural tablet dimensions. @@ -402,9 +401,10 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) // update webOverlay var RAYPICK_OFFSET = 0.0007; // Sufficient for raypick to reliably intersect tablet screen before tablet model. var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) * sensorScaleOffsetOverride + RAYPICK_OFFSET; - var WEB_ENTITY_Y_OFFSET = 0.004 * sensorScaleFactor * sensorScaleOffsetOverride; - var screenWidth = 0.82 * tabletWidth; - var screenHeight = 0.81 * tabletHeight; + var WEB_ENTITY_Y_OFFSET = 1 * tabletScaleFactor; + print(WEB_ENTITY_Y_OFFSET); + var screenWidth = 0.9275 * tabletWidth; + var screenHeight = 0.8983 * tabletHeight; var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape; Overlays.editOverlay(HMD.tabletScreenID, { localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, @@ -413,7 +413,7 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) }); // update homeButton - var HOME_BUTTON_Y_OFFSET = ((tabletHeight / 2) - (tabletHeight / 20) - 0.003 * sensorScaleFactor) * sensorScaleOffsetOverride; + var HOME_BUTTON_Y_OFFSET = ((tabletHeight / 2) - (tabletHeight / 20) + 0.003 * sensorScaleFactor) * sensorScaleOffsetOverride; // FIXME: Circle3D overlays currently at the wrong dimensions, so we need to account for that here var homeButtonDim = 4.0 * tabletScaleFactor / 3.0; Overlays.editOverlay(HMD.homeButtonID, { From a8d6a4e0b6996a57ae5128407d16e50b2982ecd9 Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 18 Apr 2018 15:05:49 -0700 Subject: [PATCH 41/48] CR --- assignment-client/src/octree/OctreeSendThread.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index f8d566862e..de49bd461c 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -416,7 +416,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* int elapsedmsec = (end - start) / USECS_PER_MSEC; OctreeServer::trackLoopTime(elapsedmsec); - // if after sending packets we've emptied our bag, then we want to remember that we've sent all + // if we've sent everything, then we want to remember that we've sent all // the octree elements from the current view frustum if (!hasSomethingToSend(nodeData)) { nodeData->setViewSent(true); @@ -465,7 +465,7 @@ void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, Octre extraPackingAttempts++; } - // If the bag had contents but is now empty then we know we've sent the entire scene. + // If we had something to send, but now we don't, then we know we've sent the entire scene. bool completedScene = hadSomething; if (completedScene || lastNodeDidntFit) { // we probably want to flush what has accumulated in nodeData but: From e01e7cc7bc3a814d4b11e3c7ce42f1ab33927ffa Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 16 Apr 2018 15:25:04 -0700 Subject: [PATCH 42/48] Be a bit more efficient --- .../resources/qml/hifi/LetterboxMessage.qml | 62 ++++++------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index c0f3fa1006..8a18d88842 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -30,6 +30,16 @@ Item { color: "black" opacity: 0.5 radius: popupRadius + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + acceptedButtons: Qt.LeftButton; + propagateComposedEvents: false; + onClicked: { + letterbox.visible = false; + } + } } Rectangle { id: textContainer; @@ -38,6 +48,14 @@ Item { anchors.centerIn: parent radius: popupRadius color: "white" + + // Prevent dismissing the popup by clicking on the textContainer + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + propagateComposedEvents: false; + } + Item { id: contentContainer width: parent.width - 50 @@ -135,48 +153,4 @@ Item { } } } - // Left gray MouseArea - MouseArea { - anchors.left: parent.left; - anchors.right: textContainer.left; - anchors.top: textContainer.top; - anchors.bottom: textContainer.bottom; - acceptedButtons: Qt.LeftButton; - onClicked: { - letterbox.visible = false; - } - } - // Right gray MouseArea - MouseArea { - anchors.left: textContainer.left; - anchors.right: parent.left; - anchors.top: textContainer.top; - anchors.bottom: textContainer.bottom; - acceptedButtons: Qt.LeftButton; - onClicked: { - letterbox.visible = false; - } - } - // Top gray MouseArea - MouseArea { - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: parent.top; - anchors.bottom: textContainer.top; - acceptedButtons: Qt.LeftButton; - onClicked: { - letterbox.visible = false; - } - } - // Bottom gray MouseArea - MouseArea { - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: textContainer.bottom; - anchors.bottom: parent.bottom; - acceptedButtons: Qt.LeftButton; - onClicked: { - letterbox.visible = false; - } - } } From 764aa0006969d56d579f0e2223dfb5d76b5c3742 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 19 Apr 2018 07:33:14 -0700 Subject: [PATCH 43/48] Coding standard fixes --- interface/src/scripting/AudioDevices.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index 34a3630b78..ee615cde20 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -188,7 +188,7 @@ void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD for (auto i = 0; i < _devices.size(); ++i) { std::shared_ptr device = _devices[i]; - bool &isSelected = isHMD ? device->selectedHMD : device->selectedDesktop; + bool& isSelected = isHMD ? device->selectedHMD : device->selectedDesktop; if (isSelected && device->info != selectedDevice) { isSelected = false; } else if (device->info == selectedDevice) { @@ -259,7 +259,7 @@ void AudioDeviceList::onDevicesChanged(const QList& devices) { foreach(const QAudioDeviceInfo& deviceInfo, devices) { for (bool isHMD : {false, true}) { - auto &backupSelectedDeviceName = isHMD ? _backupSelectedHMDDeviceName : _backupSelectedDesktopDeviceName; + auto& backupSelectedDeviceName = isHMD ? _backupSelectedHMDDeviceName : _backupSelectedDesktopDeviceName; if (deviceInfo.deviceName() == backupSelectedDeviceName) { QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice; selectedDevice = deviceInfo; @@ -278,7 +278,7 @@ void AudioDeviceList::onDevicesChanged(const QList& devices) { for (bool isHMD : {false, true}) { QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice; - bool &isSelected = isHMD ? device.selectedHMD : device.selectedDesktop; + bool& isSelected = isHMD ? device.selectedHMD : device.selectedDesktop; if (!selectedDevice.isNull()) { isSelected = (device.info == selectedDevice); From bef4eb1d05c8a6b98dbb7fdcf9d1f20585bf0de3 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 19 Apr 2018 14:03:51 -0700 Subject: [PATCH 44/48] Increase packet version for Ping --- libraries/networking/src/udt/PacketHeaders.cpp | 2 ++ libraries/networking/src/udt/PacketHeaders.h | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index a83924ee58..11b2c516f8 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -75,6 +75,8 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(IcePingVersion::SendICEPeerID); case PacketType::DomainSettings: return 18; // replace min_avatar_scale and max_avatar_scale with min_avatar_height and max_avatar_height + case PacketType::Ping: + return static_cast(PingVersion::IncludeConnectionID); default: return 17; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 09fd31a41e..091fcb1091 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -322,4 +322,8 @@ enum class IcePingVersion : PacketVersion { SendICEPeerID = 18 }; +enum class PingVersion : PacketVersion { + IncludeConnectionID = 18 +}; + #endif // hifi_PacketHeaders_h From 0552a7242a7d066b9077c8ecb88bd2dceff2548d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 Apr 2018 09:52:35 +1200 Subject: [PATCH 45/48] Further JSDoc updates for serverless domains --- libraries/networking/src/AddressManager.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 6c6070914f..0009bef49a 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -34,15 +34,16 @@ const QString GET_PLACE = "/api/v1/places/%1"; * * @namespace location * @property {Uuid} domainID - A UUID uniquely identifying the domain you're visiting. Is {@link Uuid|Uuid.NULL} if you're not - * connected to the domain. + * connected to the domain or are at a serverless domain. * Read-only. * @property {Uuid} domainId - Synonym for domainId. Read-only. Deprecated: This property * is deprecated and will soon be removed. * @property {string} hostname - The name of the domain for your current metaverse address (e.g., "AvatarIsland", - * localhost, an IP address, or the file path to a serverless domain). + * localhost, or an IP address). Is blank if you're at a serverless domain. * Read-only. * @property {string} href - Your current metaverse address (e.g., "hifi://avatarisland/15,-10,26/0,0,0,1") - * regardless of whether or not you're connected to the domain. + * regardless of whether or not you're connected to the domain. Starts with "file:///" if at a serverless + * domain. * Read-only. * @property {boolean} isConnected - true if you're connected to the domain in your current href * metaverse address, otherwise false. From 8ade4f34f4c70fe6fdb00ad82c0ba9deab22617b Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 20 Apr 2018 10:11:01 +1200 Subject: [PATCH 46/48] Tweak wording --- libraries/networking/src/AddressManager.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 0009bef49a..94eff46bda 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -34,16 +34,16 @@ const QString GET_PLACE = "/api/v1/places/%1"; * * @namespace location * @property {Uuid} domainID - A UUID uniquely identifying the domain you're visiting. Is {@link Uuid|Uuid.NULL} if you're not - * connected to the domain or are at a serverless domain. + * connected to the domain or are in a serverless domain. * Read-only. * @property {Uuid} domainId - Synonym for domainId. Read-only. Deprecated: This property * is deprecated and will soon be removed. * @property {string} hostname - The name of the domain for your current metaverse address (e.g., "AvatarIsland", - * localhost, or an IP address). Is blank if you're at a serverless domain. + * localhost, or an IP address). Is blank if you're in a serverless domain. * Read-only. * @property {string} href - Your current metaverse address (e.g., "hifi://avatarisland/15,-10,26/0,0,0,1") - * regardless of whether or not you're connected to the domain. Starts with "file:///" if at a serverless - * domain. + * regardless of whether or not you're connected to the domain. Starts with "file:///" if you're in a + * serverless domain. * Read-only. * @property {boolean} isConnected - true if you're connected to the domain in your current href * metaverse address, otherwise false. From 377fc6d6c6d8041550d4b2c9919a54d42f6865cb Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 19 Apr 2018 16:55:32 -0700 Subject: [PATCH 47/48] Revert to old packet version for ICE packets Some ICE (& STUN) packets were using the default version, which we bumped for the HMAC change. This commit breaks out the switch for them and reverts to 17. --- libraries/networking/src/udt/PacketHeaders.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index f09a049fc4..0dbeb1e92e 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -24,6 +24,8 @@ int packetTypeMetaTypeId = qRegisterMetaType(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { + case PacketType::StunResponse: + return 17; case PacketType::DomainList: return static_cast(DomainListVersion::GetMachineFingerprintFromUUIDSupport); case PacketType::EntityAdd: @@ -40,8 +42,21 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(AvatarMixerPacketVersion::FBXReaderNodeReparenting); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); + // ICE packets + case PacketType::ICEServerPeerInformation: + return 17; + case PacketType::ICEServerHeartbeatACK: + return 17; + case PacketType::ICEServerQuery: + return 17; case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing + case PacketType::ICEPing: + return static_cast(IcePingVersion::SendICEPeerID); + case PacketType::ICEPingReply: + return 17; + case PacketType::ICEServerHeartbeatDenied: + return 17; case PacketType::AssetMappingOperation: case PacketType::AssetMappingOperationReply: return static_cast(AssetServerPacketVersion::RedirectedMappings); @@ -71,8 +86,6 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::MicrophoneAudioWithEcho: case PacketType::AudioStreamStats: return static_cast(AudioVersion::HighDynamicRangeVolume); - case PacketType::ICEPing: - return static_cast(IcePingVersion::SendICEPeerID); case PacketType::DomainSettings: return 18; // replace min_avatar_scale and max_avatar_scale with min_avatar_height and max_avatar_height default: From 3b4b43cf66d8ea45f5386fdf9359a6674bd7f61e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 18 Apr 2018 13:29:39 -0700 Subject: [PATCH 48/48] try to fix audio crash on startup --- interface/src/Application.cpp | 13 +- interface/src/scripting/Audio.cpp | 142 ++++++++++++++------- interface/src/scripting/Audio.h | 20 ++- libraries/audio-client/src/AudioClient.cpp | 24 ++-- libraries/audio-client/src/AudioClient.h | 10 +- 5 files changed, 134 insertions(+), 75 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index aec31f2de4..cfd1d4412c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1102,10 +1102,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto audioScriptingInterface = DependencyManager::get(); auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getWorldPosition(); float distance = glm::distance(myAvatarPosition, position); - bool shouldMute = !audioClient->isMuted() && (distance < radius); - if (shouldMute) { - audioClient->toggleMute(); + if (distance < radius) { + audioClient->setMuted(true); audioScriptingInterface->environmentMuted(); } }); @@ -1508,7 +1507,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo if (state) { if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { - DependencyManager::get()->toggleMute(); + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { cycleCamera(); } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { @@ -3461,7 +3461,8 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_M: if (isMeta) { - DependencyManager::get()->toggleMute(); + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); } break; @@ -5119,7 +5120,7 @@ void Application::update(float deltaTime) { if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) { if (_lastFaceTrackerUpdate > 0 && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { - audioClient->toggleMute(); + audioClient->setMuted(true); _lastFaceTrackerUpdate = 0; } } else { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index be9b4280f7..387900b2ae 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -50,110 +50,162 @@ float Audio::loudnessToLevel(float loudness) { Audio::Audio() : _devices(_contextIsHMD) { auto client = DependencyManager::get().data(); - connect(client, &AudioClient::muteToggled, this, &Audio::onMutedChanged); - connect(client, &AudioClient::noiseReductionChanged, this, &Audio::onNoiseReductionChanged); + connect(client, &AudioClient::muteToggled, this, &Audio::setMuted); + connect(client, &AudioClient::noiseReductionChanged, this, &Audio::enableNoiseReduction); connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); - connect(client, &AudioClient::inputVolumeChanged, this, &Audio::onInputVolumeChanged); + connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); enableNoiseReduction(enableNoiseReductionSetting.get()); } bool Audio::startRecording(const QString& filepath) { auto client = DependencyManager::get().data(); - return client->startRecording(filepath); + return resultWithWriteLock([&] { + return client->startRecording(filepath); + }); } bool Audio::getRecording() { auto client = DependencyManager::get().data(); - return client->getRecording(); + return resultWithReadLock([&] { + return client->getRecording(); + }); } void Audio::stopRecording() { auto client = DependencyManager::get().data(); - client->stopRecording(); + withWriteLock([&] { + client->stopRecording(); + }); +} + +bool Audio::isMuted() const { + return resultWithReadLock([&] { + return _isMuted; + }); } void Audio::setMuted(bool isMuted) { - if (_isMuted != isMuted) { - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "toggleMute"); + bool changed = false; + withWriteLock([&] { + if (_isMuted != isMuted) { + _isMuted = isMuted; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); + changed = true; + } + }); + if (changed) { + emit mutedChanged(isMuted); } } -void Audio::onMutedChanged() { - bool isMuted = DependencyManager::get()->isMuted(); - if (_isMuted != isMuted) { - _isMuted = isMuted; - emit mutedChanged(_isMuted); - } +bool Audio::noiseReductionEnabled() const { + return resultWithReadLock([&] { + return _enableNoiseReduction; + }); } void Audio::enableNoiseReduction(bool enable) { - if (_enableNoiseReduction != enable) { - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setNoiseReduction", Q_ARG(bool, enable)); - enableNoiseReductionSetting.set(enable); + bool changed = false; + withWriteLock([&] { + if (_enableNoiseReduction != enable) { + _enableNoiseReduction = enable; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setNoiseReduction", Q_ARG(bool, enable), Q_ARG(bool, false)); + enableNoiseReductionSetting.set(enable); + changed = true; + } + }); + if (changed) { + emit noiseReductionChanged(enable); } } -void Audio::onNoiseReductionChanged() { - bool noiseReductionEnabled = DependencyManager::get()->isNoiseReductionEnabled(); - if (_enableNoiseReduction != noiseReductionEnabled) { - _enableNoiseReduction = noiseReductionEnabled; - emit noiseReductionChanged(_enableNoiseReduction); - } +float Audio::getInputVolume() const { + return resultWithReadLock([&] { + return _inputVolume; + }); } void Audio::setInputVolume(float volume) { // getInputVolume will not reflect changes synchronously, so clamp beforehand volume = glm::clamp(volume, 0.0f, 1.0f); - if (_inputVolume != volume) { - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setInputVolume", Q_ARG(float, volume)); + bool changed = false; + withWriteLock([&] { + if (_inputVolume != volume) { + _inputVolume = volume; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setInputVolume", Q_ARG(float, volume), Q_ARG(bool, false)); + changed = true; + } + }); + if (changed) { + emit inputVolumeChanged(volume); } } -void Audio::onInputVolumeChanged(float volume) { - if (_inputVolume != volume) { - _inputVolume = volume; - emit inputVolumeChanged(_inputVolume); - } +float Audio::getInputLevel() const { + return resultWithReadLock([&] { + return _inputLevel; + }); } void Audio::onInputLoudnessChanged(float loudness) { float level = loudnessToLevel(loudness); - - if (_inputLevel != level) { - _inputLevel = level; - emit inputLevelChanged(_inputLevel); + bool changed = false; + withWriteLock([&] { + if (_inputLevel != level) { + _inputLevel = level; + changed = true; + } + }); + if (changed) { + emit inputLevelChanged(level); } } QString Audio::getContext() const { - return _contextIsHMD ? Audio::HMD : Audio::DESKTOP; + return resultWithReadLock([&] { + return _contextIsHMD ? Audio::HMD : Audio::DESKTOP; + }); } void Audio::onContextChanged() { + bool changed = false; bool isHMD = qApp->isHMDMode(); - if (_contextIsHMD != isHMD) { - _contextIsHMD = isHMD; - emit contextChanged(getContext()); + withWriteLock([&] { + if (_contextIsHMD != isHMD) { + _contextIsHMD = isHMD; + changed = true; + } + }); + if (changed) { + emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); } } void Audio::setReverb(bool enable) { - DependencyManager::get()->setReverb(enable); + withWriteLock([&] { + DependencyManager::get()->setReverb(enable); + }); } void Audio::setReverbOptions(const AudioEffectOptions* options) { - DependencyManager::get()->setReverbOptions(options); + withWriteLock([&] { + DependencyManager::get()->setReverbOptions(options); + }); } void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) { - _devices.chooseInputDevice(device, isHMD); + withWriteLock([&] { + _devices.chooseInputDevice(device, isHMD); + }); } void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) { - _devices.chooseOutputDevice(device, isHMD); + withWriteLock([&] { + _devices.chooseOutputDevice(device, isHMD); + }); } diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 0f0043510c..b4e63b80c5 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -17,10 +17,11 @@ #include "AudioEffectOptions.h" #include "SettingHandle.h" #include "AudioFileWav.h" +#include namespace scripting { -class Audio : public AudioScriptingInterface { +class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_OBJECT SINGLETON_DEPENDENCY @@ -40,16 +41,13 @@ public: virtual ~Audio() {} - bool isMuted() const { return _isMuted; } - bool noiseReductionEnabled() const { return _enableNoiseReduction; } - float getInputVolume() const { return _inputVolume; } - float getInputLevel() const { return _inputLevel; } + bool isMuted() const; + bool noiseReductionEnabled() const; + float getInputVolume() const; + float getInputLevel() const; QString getContext() const; - void setMuted(bool muted); - void enableNoiseReduction(bool enable); void showMicMeter(bool show); - void setInputVolume(float volume); Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device, bool isHMD); Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device, bool isHMD); @@ -72,9 +70,9 @@ public slots: void onContextChanged(); private slots: - void onMutedChanged(); - void onNoiseReductionChanged(); - void onInputVolumeChanged(float volume); + void setMuted(bool muted); + void enableNoiseReduction(bool enable); + void setInputVolume(float volume); void onInputLoudnessChanged(float loudness); protected: diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index e2dae92a94..f643719a2e 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -757,7 +757,7 @@ void AudioClient::Gate::flush() { void AudioClient::handleNoisyMutePacket(QSharedPointer message) { if (!_muted) { - toggleMute(); + setMuted(true); // have the audio scripting interface emit a signal to say we were muted by the mixer emit mutedByMixer(); @@ -1384,15 +1384,21 @@ void AudioClient::sendMuteEnvironmentPacket() { } } -void AudioClient::toggleMute() { - _muted = !_muted; - emit muteToggled(); +void AudioClient::setMuted(bool muted, bool emitSignal) { + if (_muted != muted) { + _muted = muted; + if (emitSignal) { + emit muteToggled(_muted); + } + } } -void AudioClient::setNoiseReduction(bool enable) { +void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { if (_isNoiseGateEnabled != enable) { _isNoiseGateEnabled = enable; - emit noiseReductionChanged(); + if (emitSignal) { + emit noiseReductionChanged(_isNoiseGateEnabled); + } } } @@ -2018,9 +2024,11 @@ void AudioClient::startThread() { moveToNewNamedThread(this, "Audio Thread", [this] { start(); }); } -void AudioClient::setInputVolume(float volume) { +void AudioClient::setInputVolume(float volume, bool emitSignal) { if (_audioInput && volume != (float)_audioInput->volume()) { _audioInput->setVolume(volume); - emit inputVolumeChanged(_audioInput->volume()); + if (emitSignal) { + emit inputVolumeChanged(_audioInput->volume()); + } } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3bfbdb49ce..9ee7bcfeba 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -189,13 +189,13 @@ public slots: void reset(); void audioMixerKilled(); - void toggleMute(); + void setMuted(bool muted, bool emitSignal = true); bool isMuted() { return _muted; } virtual bool setIsStereoInput(bool stereo) override; virtual bool isStereoInput() override { return _isStereoInput; } - void setNoiseReduction(bool isNoiseGateEnabled); + void setNoiseReduction(bool isNoiseGateEnabled, bool emitSignal = true); bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } bool getLocalEcho() { return _shouldEchoLocally; } @@ -218,7 +218,7 @@ public slots: bool switchAudioDevice(QAudio::Mode mode, const QString& deviceName); float getInputVolume() const { return (_audioInput) ? (float)_audioInput->volume() : 0.0f; } - void setInputVolume(float volume); + void setInputVolume(float volume, bool emitSignal = true); void setReverb(bool reverb); void setReverbOptions(const AudioEffectOptions* options); @@ -229,8 +229,8 @@ public slots: signals: void inputVolumeChanged(float volume); - void muteToggled(); - void noiseReductionChanged(); + void muteToggled(bool muted); + void noiseReductionChanged(bool noiseReductionEnabled); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); void inputLoudnessChanged(float loudness);