From 92756d81ad8296b1c6caa3b4bcdaa16080e53877 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 12 Jul 2018 15:03:01 -0700 Subject: [PATCH] Implement MS16562: Massive PAL speedup! --- interface/resources/qml/hifi/Pal.qml | 4 +- interface/src/avatar/AvatarManager.cpp | 39 +++++++ interface/src/avatar/AvatarManager.h | 2 + scripts/system/pal.js | 148 ++++++++++++------------- 4 files changed, 112 insertions(+), 81 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 3059707313..06ad57139d 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -1108,7 +1108,7 @@ Rectangle { function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml var data = optionalData || nearbyUserModelData, length = data.length; for (var i = 0; i < length; i++) { - if (data[i].sessionId === sessionId) { + if (data[i].sessionId === sessionId.toString()) { return i; } } @@ -1120,7 +1120,7 @@ Rectangle { var data = message.params; var index = -1; iAmAdmin = Users.canKick; - index = findNearbySessionIndex('', data); + index = findNearbySessionIndex(MyAvatar.sessionUUID, data); if (index !== -1) { myData = data[index]; data.splice(index, 1); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 29ad71ead6..a7ee5f4869 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "AvatarLogging.h" @@ -668,3 +669,41 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV DependencyManager::get()->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); } } + +QString AvatarManager::getPalData(const QList specificAvatarIdentifiers) { + QJsonArray palData; + + auto avatarMap = getHashCopy(); + AvatarHash::iterator itr = avatarMap.begin(); + while (itr != avatarMap.end()) { + std::shared_ptr avatar = std::static_pointer_cast(*itr); + QString currentSessionUUID = avatar->getSessionUUID().toString(); + if (specificAvatarIdentifiers.isEmpty() || specificAvatarIdentifiers.contains(currentSessionUUID)) { + QJsonObject thisAvatarPalData; + thisAvatarPalData.insert("sessionUUID", currentSessionUUID); + thisAvatarPalData.insert("sessionDisplayName", avatar->getSessionDisplayName()); + thisAvatarPalData.insert("audioLoudness", avatar->getAudioLoudness()); + thisAvatarPalData.insert("isReplicated", avatar->getIsReplicated()); + + glm::vec3 position = avatar->getWorldPosition(); + QJsonObject jsonPosition; + jsonPosition.insert("x", position.x); + jsonPosition.insert("y", position.y); + jsonPosition.insert("z", position.z); + thisAvatarPalData.insert("position", jsonPosition); + + float palOrbOffset = 0.2f; + int headIndex = avatar->getJointIndex("Head"); + if (headIndex > 0) { + glm::vec3 jointTranslation = avatar->getAbsoluteJointTranslationInObjectFrame(headIndex); + palOrbOffset = jointTranslation.y / 2; + } + thisAvatarPalData.insert("palOrbOffset", palOrbOffset); + + palData.append(thisAvatarPalData); + } + ++itr; + } + QJsonDocument doc(palData); + return doc.toJson(QJsonDocument::Compact); +} diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index ff29c1b381..507767866b 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -157,6 +157,8 @@ public: */ Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value); + Q_INVOKABLE QString getPalData(const QList specificAvatarIdentifiers = QList()); + float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); } int getIdentityRequestsSent() const { return _identityRequestsSent; } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index e967ee6469..4a0d866d66 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -19,7 +19,7 @@ var populateNearbyUserList, color, textures, removeOverlays, controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, - createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged, + tablet, CHANNEL, getConnectionData, findableByChanged, avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed @@ -447,21 +447,24 @@ function populateNearbyUserList(selectData, oldAudioData) { verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); avatarsOfInterest = {}; - avatars.forEach(function (id) { - var avatar = AvatarList.getAvatar(id); - var name = avatar.sessionDisplayName; + + var avatarData = JSON.parse(AvatarList.getPalData()); + + avatarData.forEach(function (currentAvatarData) { + var id = currentAvatarData.sessionUUID; + var name = currentAvatarData.sessionDisplayName; if (!name) { // Either we got a data packet but no identity yet, or something is really messed up. In any case, // we won't be able to do anything with this user, so don't include them. // In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded, // we could be losing and gaining people randomly. - print('No avatar identity data for', id); + print('No avatar identity data for', currentAvatarData.sessionUUID); return; } - if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) { + if (id && myPosition && (Vec3.distance(currentAvatarData.position, myPosition) > filter.distance)) { return; } - var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); + var normal = id && filter && Vec3.normalize(Vec3.subtract(currentAvatarData.position, myPosition)); var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { @@ -480,11 +483,11 @@ function populateNearbyUserList(selectData, oldAudioData) { personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null ignore: !!id && Users.getIgnoreStatus(id), // ditto isPresent: true, - isReplicated: avatar.isReplicated + isReplicated: currentAvatarData.isReplicated }; // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. Users.requestUsernameFromID(id); - if (id) { + if (id !== MyAvatar.sessionUUID) { addAvatarNode(id); // No overlay for ourselves avatarsOfInterest[id] = true; } else { @@ -515,30 +518,60 @@ function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { updateUser(data); } +function updateAudioLevel(overlay, avatarData) { + // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged + // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency + // of updating (the latter for efficiency too). + var audioLevel = 0.0; + var avgAudioLevel = 0.0; + + // we will do exponential moving average by taking some the last loudness and averaging + overlay.accumulatedLevel = AVERAGING_RATIO * (overlay.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatarData.audioLoudness); + + // add 1 to insure we don't go log() and hit -infinity. Math.log is + // natural log, so to get log base 2, just divide by ln(2). + audioLevel = scaleAudio(Math.log(overlay.accumulatedLevel + 1) / LOG2); + + // decay avgAudioLevel + avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (overlay.avgAudioLevel || 0), audioLevel); + + overlay.avgAudioLevel = avgAudioLevel; + overlay.audioLevel = audioLevel; + + // now scale for the gain. Also, asked to boost the low end, so one simple way is + // to take sqrt of the value. Lets try that, see how it feels. + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[avatarData.sessionUUID] || 0.75))); + + + var param = {}; + var level = [audioLevel, avgAudioLevel]; + var userId = avatarData.sessionUUID || 0; + param[userId] = level; + sendToQml({ method: 'updateAudioLevel', params: param }); +} + var pingPong = true; function updateOverlays() { var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id || !avatarsOfInterest[id]) { + + var avatarData = JSON.parse(AvatarList.getPalData()); + + avatarData.forEach(function (currentAvatarData) { + if (currentAvatarData.sessionUUID === MyAvatar.sessionUUID || !avatarsOfInterest[currentAvatarData.sessionUUID]) { return; // don't update ourself, or avatars we're not interested in } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); + var overlay = ExtendedOverlay.get(currentAvatarData.sessionUUID); if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - print('Adding non-PAL avatar node', id); - overlay = addAvatarNode(id); + print('Adding non-PAL avatar node', currentAvatarData.sessionUUID); + overlay = addAvatarNode(currentAvatarData.sessionUUID); } - var target = avatar.position; + + updateAudioLevel(overlay, currentAvatarData); + + var target = currentAvatarData.position; var distance = Vec3.distance(target, eye); - var offset = 0.2; + var offset = currentAvatarData.palOrbOffset; var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); @@ -548,7 +581,7 @@ function updateOverlays() { overlay.ping = pingPong; overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel), + color: color(ExtendedOverlay.isSelected(currentAvatarData.sessionUUID), overlay.hovering, overlay.audioLevel), position: target, dimensions: 0.032 * distance }); @@ -704,6 +737,13 @@ function wireEventBridge(on) { hasEventBridge = false; } } + } + +var UPDATE_INTERVAL_MS = 100; +function createUpdateInterval() { + return Script.setInterval(function () { + updateOverlays(); + }, UPDATE_INTERVAL_MS); } function onTabletScreenChanged(type, url) { @@ -719,10 +759,8 @@ function onTabletScreenChanged(type, url) { ContextOverlay.enabled = false; Users.requestsDomainListData = true; - audioTimer = createAudioInterval(AUDIO_LEVEL_UPDATE_INTERVAL_MS); - tablet.tabletShownChanged.connect(tabletVisibilityChanged); - Script.update.connect(updateOverlays); + updateInterval = createUpdateInterval(); Controller.mousePressEvent.connect(handleMouseEvent); Controller.mouseMoveEvent.connect(handleMouseMoveEvent); Users.usernameFromIDReply.connect(usernameFromIDReply); @@ -778,50 +816,6 @@ function scaleAudio(val) { return audioLevel; } -function getAudioLevel(id) { - // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged - // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency - // of updating (the latter for efficiency too). - var avatar = AvatarList.getAvatar(id); - var audioLevel = 0.0; - var avgAudioLevel = 0.0; - var data = id ? ExtendedOverlay.get(id) : myData; - if (data) { - - // we will do exponential moving average by taking some the last loudness and averaging - data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); - - // add 1 to insure we don't go log() and hit -infinity. Math.log is - // natural log, so to get log base 2, just divide by ln(2). - audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); - - // decay avgAudioLevel - avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); - - data.avgAudioLevel = avgAudioLevel; - data.audioLevel = audioLevel; - - // now scale for the gain. Also, asked to boost the low end, so one simple way is - // to take sqrt of the value. Lets try that, see how it feels. - avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); - } - return [audioLevel, avgAudioLevel]; -} - -function createAudioInterval(interval) { - // we will update the audioLevels periodically - // TODO: tune for efficiency - expecially with large numbers of avatars - return Script.setInterval(function () { - var param = {}; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id), - userId = id || 0; // qml didn't like an object with null/empty string for a key, so... - param[userId] = level; - }); - sendToQml({method: 'updateAudioLevel', params: param}); - }, interval); -} - function avatarDisconnected(nodeID) { // remove from the pal list sendToQml({method: 'avatarDisconnected', params: [nodeID]}); @@ -874,11 +868,11 @@ startup(); var isWired = false; -var audioTimer; -var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) function off() { if (isWired) { - Script.update.disconnect(updateOverlays); + if (updateInterval) { + Script.clearInterval(updateInterval); + } Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); @@ -889,10 +883,6 @@ function off() { Users.requestsDomainListData = false; isWired = false; - - if (audioTimer) { - Script.clearInterval(audioTimer); - } } removeOverlays();