Implement MS16562: Massive PAL speedup!

This commit is contained in:
Zach Fox 2018-07-12 15:03:01 -07:00
parent de33ca512f
commit 92756d81ad
4 changed files with 112 additions and 81 deletions

View file

@ -1108,7 +1108,7 @@ Rectangle {
function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml
var data = optionalData || nearbyUserModelData, length = data.length; var data = optionalData || nearbyUserModelData, length = data.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if (data[i].sessionId === sessionId) { if (data[i].sessionId === sessionId.toString()) {
return i; return i;
} }
} }
@ -1120,7 +1120,7 @@ Rectangle {
var data = message.params; var data = message.params;
var index = -1; var index = -1;
iAmAdmin = Users.canKick; iAmAdmin = Users.canKick;
index = findNearbySessionIndex('', data); index = findNearbySessionIndex(MyAvatar.sessionUUID, data);
if (index !== -1) { if (index !== -1) {
myData = data[index]; myData = data[index];
data.splice(index, 1); data.splice(index, 1);

View file

@ -14,6 +14,7 @@
#include <string> #include <string>
#include <QScriptEngine> #include <QScriptEngine>
#include <QtCore/QJsonDocument>
#include "AvatarLogging.h" #include "AvatarLogging.h"
@ -668,3 +669,41 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV
DependencyManager::get<NodeList>()->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); DependencyManager::get<NodeList>()->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer);
} }
} }
QString AvatarManager::getPalData(const QList<QString> specificAvatarIdentifiers) {
QJsonArray palData;
auto avatarMap = getHashCopy();
AvatarHash::iterator itr = avatarMap.begin();
while (itr != avatarMap.end()) {
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(*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);
}

View file

@ -157,6 +157,8 @@ public:
*/ */
Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value); Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value);
Q_INVOKABLE QString getPalData(const QList<QString> specificAvatarIdentifiers = QList<QString>());
float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); } float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); }
int getIdentityRequestsSent() const { return _identityRequestsSent; } int getIdentityRequestsSent() const { return _identityRequestsSent; }

View file

@ -19,7 +19,7 @@
var populateNearbyUserList, color, textures, removeOverlays, var populateNearbyUserList, color, textures, removeOverlays,
controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged,
receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL,
createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged, tablet, CHANNEL, getConnectionData, findableByChanged,
avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; avatarAdded, avatarRemoved, avatarSessionChanged; // forward references;
// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed // 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), verticalAngleNormal = filter && Quat.getRight(orientation),
horizontalAngleNormal = filter && Quat.getUp(orientation); horizontalAngleNormal = filter && Quat.getUp(orientation);
avatarsOfInterest = {}; avatarsOfInterest = {};
avatars.forEach(function (id) {
var avatar = AvatarList.getAvatar(id); var avatarData = JSON.parse(AvatarList.getPalData());
var name = avatar.sessionDisplayName;
avatarData.forEach(function (currentAvatarData) {
var id = currentAvatarData.sessionUUID;
var name = currentAvatarData.sessionDisplayName;
if (!name) { if (!name) {
// Either we got a data packet but no identity yet, or something is really messed up. In any case, // 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. // 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, // 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. // we could be losing and gaining people randomly.
print('No avatar identity data for', id); print('No avatar identity data for', currentAvatarData.sessionUUID);
return; return;
} }
if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) { if (id && myPosition && (Vec3.distance(currentAvatarData.position, myPosition) > filter.distance)) {
return; 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 horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal);
var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal);
if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { 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 personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null
ignore: !!id && Users.getIgnoreStatus(id), // ditto ignore: !!id && Users.getIgnoreStatus(id), // ditto
isPresent: true, 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. // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin.
Users.requestUsernameFromID(id); Users.requestUsernameFromID(id);
if (id) { if (id !== MyAvatar.sessionUUID) {
addAvatarNode(id); // No overlay for ourselves addAvatarNode(id); // No overlay for ourselves
avatarsOfInterest[id] = true; avatarsOfInterest[id] = true;
} else { } else {
@ -515,30 +518,60 @@ function usernameFromIDReply(id, username, machineFingerprint, isAdmin) {
updateUser(data); 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; var pingPong = true;
function updateOverlays() { function updateOverlays() {
var eye = Camera.position; 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 return; // don't update ourself, or avatars we're not interested in
} }
var avatar = AvatarList.getAvatar(id); var overlay = ExtendedOverlay.get(currentAvatarData.sessionUUID);
if (!avatar) {
return; // will be deleted below if there had been an overlay.
}
var overlay = ExtendedOverlay.get(id);
if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. 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); print('Adding non-PAL avatar node', currentAvatarData.sessionUUID);
overlay = addAvatarNode(id); overlay = addAvatarNode(currentAvatarData.sessionUUID);
} }
var target = avatar.position;
updateAudioLevel(overlay, currentAvatarData);
var target = currentAvatarData.position;
var distance = Vec3.distance(target, eye); 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 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 // move a bit in front, towards the camera
target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset));
@ -548,7 +581,7 @@ function updateOverlays() {
overlay.ping = pingPong; overlay.ping = pingPong;
overlay.editOverlay({ overlay.editOverlay({
color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel), color: color(ExtendedOverlay.isSelected(currentAvatarData.sessionUUID), overlay.hovering, overlay.audioLevel),
position: target, position: target,
dimensions: 0.032 * distance dimensions: 0.032 * distance
}); });
@ -704,6 +737,13 @@ function wireEventBridge(on) {
hasEventBridge = false; hasEventBridge = false;
} }
} }
}
var UPDATE_INTERVAL_MS = 100;
function createUpdateInterval() {
return Script.setInterval(function () {
updateOverlays();
}, UPDATE_INTERVAL_MS);
} }
function onTabletScreenChanged(type, url) { function onTabletScreenChanged(type, url) {
@ -719,10 +759,8 @@ function onTabletScreenChanged(type, url) {
ContextOverlay.enabled = false; ContextOverlay.enabled = false;
Users.requestsDomainListData = true; Users.requestsDomainListData = true;
audioTimer = createAudioInterval(AUDIO_LEVEL_UPDATE_INTERVAL_MS);
tablet.tabletShownChanged.connect(tabletVisibilityChanged); tablet.tabletShownChanged.connect(tabletVisibilityChanged);
Script.update.connect(updateOverlays); updateInterval = createUpdateInterval();
Controller.mousePressEvent.connect(handleMouseEvent); Controller.mousePressEvent.connect(handleMouseEvent);
Controller.mouseMoveEvent.connect(handleMouseMoveEvent); Controller.mouseMoveEvent.connect(handleMouseMoveEvent);
Users.usernameFromIDReply.connect(usernameFromIDReply); Users.usernameFromIDReply.connect(usernameFromIDReply);
@ -778,50 +816,6 @@ function scaleAudio(val) {
return audioLevel; 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) { function avatarDisconnected(nodeID) {
// remove from the pal list // remove from the pal list
sendToQml({method: 'avatarDisconnected', params: [nodeID]}); sendToQml({method: 'avatarDisconnected', params: [nodeID]});
@ -874,11 +868,11 @@ startup();
var isWired = false; 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() { function off() {
if (isWired) { if (isWired) {
Script.update.disconnect(updateOverlays); if (updateInterval) {
Script.clearInterval(updateInterval);
}
Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mousePressEvent.disconnect(handleMouseEvent);
Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent);
tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
@ -889,10 +883,6 @@ function off() {
Users.requestsDomainListData = false; Users.requestsDomainListData = false;
isWired = false; isWired = false;
if (audioTimer) {
Script.clearInterval(audioTimer);
}
} }
removeOverlays(); removeOverlays();