From 8ab6974233572515f588a4ab148f6287ac60366d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 15 Dec 2016 14:32:55 -0800 Subject: [PATCH 01/68] optimizations for processing avatar joint data --- interface/src/Application.cpp | 11 ++++- interface/src/avatar/Avatar.cpp | 82 ++++++++++++++++++++------------- libraries/animation/src/Rig.cpp | 73 ++++++++++++++++++----------- libraries/animation/src/Rig.h | 1 + 4 files changed, 106 insertions(+), 61 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b4b0ad10bb..e32e9e90b1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2572,6 +2572,7 @@ bool Application::eventFilter(QObject* object, QEvent* event) { } static bool _altPressed{ false }; +static bool dumpAllTimerRecords { false }; // adebug hack void Application::keyPressEvent(QKeyEvent* event) { _altPressed = event->key() == Qt::Key_Alt; @@ -2662,7 +2663,8 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_F: { - _physicsEngine->dumpNextStats(); + //_physicsEngine->dumpNextStats(); + dumpAllTimerRecords = true; // adebug hack break; } @@ -3408,6 +3410,11 @@ void Application::idle(float nsecsElapsed) { } _overlayConductor.update(secondsSinceLastUpdate); + // adebug hack + if (dumpAllTimerRecords) { + PerformanceTimer::dumpAllTimerRecords(); + dumpAllTimerRecords = false; + } } void Application::setLowVelocityFilter(bool lowVelocityFilter) { @@ -4307,7 +4314,7 @@ void Application::update(float deltaTime) { // AvatarManager update { - PerformanceTimer perfTimer("AvatarManger"); + PerformanceTimer perfTimer("AvatarManager"); _avatarSimCounter.increment(); { diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 2f99ccfdc8..908b3def8d 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -275,13 +275,16 @@ void Avatar::updateAvatarEntities() { } AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); - _avatarEntitiesLock.withReadLock([&] { - foreach (auto entityID, recentlyDettachedAvatarEntities) { - if (!_avatarEntityData.contains(entityID)) { - entityTree->deleteEntity(entityID, true, true); + if (!recentlyDettachedAvatarEntities.empty()) { + // only lock this thread when absolutely necessary + _avatarEntitiesLock.withReadLock([&] { + foreach (auto entityID, recentlyDettachedAvatarEntities) { + if (!_avatarEntityData.contains(entityID)) { + entityTree->deleteEntity(entityID, true, true); + } } - } - }); + }); + } }); if (success) { @@ -299,18 +302,25 @@ void Avatar::simulate(float deltaTime) { } animateScaleChanges(deltaTime); - bool avatarPositionInView = false; - bool avatarMeshInView = false; + bool avatarInView = false; { // update the shouldAnimate flag to match whether or not we will render the avatar. PerformanceTimer perfTimer("cull"); - ViewFrustum viewFrustum; { - PerformanceTimer perfTimer("LOD"); + // simple frustum check + PerformanceTimer perfTimer("inView"); + ViewFrustum viewFrustum; + qApp->copyDisplayViewFrustum(viewFrustum); + avatarInView = viewFrustum.sphereIntersectsFrustum(getPosition(), getBoundingRadius()) + || viewFrustum.boxIntersectsFrustum(_skeletonModel->getRenderableMeshBound()); + } + PerformanceTimer lodPerfTimer("LOD"); + if (avatarInView) { const float MINIMUM_VISIBILITY_FOR_ON = 0.4f; const float MAXIMUM_VISIBILITY_FOR_OFF = 0.6f; + ViewFrustum viewFrustum; qApp->copyViewFrustum(viewFrustum); float visibility = calculateRenderAccuracy(viewFrustum.getPosition(), - getBounds(), DependencyManager::get()->getOctreeSizeScale()); + getBounds(), DependencyManager::get()->getOctreeSizeScale()); if (!_shouldAnimate) { if (visibility > MINIMUM_VISIBILITY_FOR_ON) { _shouldAnimate = true; @@ -321,19 +331,11 @@ void Avatar::simulate(float deltaTime) { qCDebug(interfaceapp) << "Optimizing" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for visibility" << visibility; } } - - { - PerformanceTimer perfTimer("inView"); - // simple frustum check - float boundingRadius = getBoundingRadius(); - qApp->copyDisplayViewFrustum(viewFrustum); - avatarPositionInView = viewFrustum.sphereIntersectsFrustum(getPosition(), boundingRadius); - avatarMeshInView = viewFrustum.boxIntersectsFrustum(_skeletonModel->getRenderableMeshBound()); - } } uint64_t start = usecTimestampNow(); - if (_shouldAnimate && !_shouldSkipRender && (avatarPositionInView || avatarMeshInView)) { + // CRUFT? _shouldSkipRender is never set 'true' + if (_shouldAnimate && avatarInView && !_shouldSkipRender) { { PerformanceTimer perfTimer("skeleton"); _skeletonModel->getRig()->copyJointsFromJointData(_jointData); @@ -643,6 +645,8 @@ glm::quat Avatar::computeRotationFromBodyToWorldUp(float proportion) const { } void Avatar::fixupModelsInScene() { +#define ADEBUG +#ifdef ADEBUG _attachmentsToDelete.clear(); // check to see if when we added our models to the scene they were ready, if they were not ready, then @@ -666,6 +670,7 @@ void Avatar::fixupModelsInScene() { _attachmentsToDelete.insert(_attachmentsToDelete.end(), _attachmentsToRemove.begin(), _attachmentsToRemove.end()); _attachmentsToRemove.clear(); scene->enqueuePendingChanges(pendingChanges); +#endif // ADEBUG } bool Avatar::shouldRenderHead(const RenderArgs* renderArgs) const { @@ -725,7 +730,7 @@ glm::vec3 Avatar::getDisplayNamePosition() const { glm::vec3 bodyUpDirection = getBodyUpDirection(); DEBUG_VALUE("bodyUpDirection =", bodyUpDirection); - if (getSkeletonModel()->getNeckPosition(namePosition)) { + if (_skeletonModel->getNeckPosition(namePosition)) { float headHeight = getHeadHeight(); DEBUG_VALUE("namePosition =", namePosition); DEBUG_VALUE("headHeight =", headHeight); @@ -1244,8 +1249,8 @@ glm::vec3 Avatar::getUncachedLeftPalmPosition() const { return leftPalmPosition; } // avatar didn't have a LeftHandMiddle1 joint, fall back on this: - getSkeletonModel()->getJointRotationInWorldFrame(getSkeletonModel()->getLeftHandJointIndex(), leftPalmRotation); - getSkeletonModel()->getLeftHandPosition(leftPalmPosition); + _skeletonModel->getJointRotationInWorldFrame(_skeletonModel->getLeftHandJointIndex(), leftPalmRotation); + _skeletonModel->getLeftHandPosition(leftPalmPosition); leftPalmPosition += HAND_TO_PALM_OFFSET * glm::inverse(leftPalmRotation); return leftPalmPosition; } @@ -1253,7 +1258,7 @@ glm::vec3 Avatar::getUncachedLeftPalmPosition() const { glm::quat Avatar::getUncachedLeftPalmRotation() const { assert(QThread::currentThread() == thread()); // main thread access only glm::quat leftPalmRotation; - getSkeletonModel()->getJointRotationInWorldFrame(getSkeletonModel()->getLeftHandJointIndex(), leftPalmRotation); + _skeletonModel->getJointRotationInWorldFrame(_skeletonModel->getLeftHandJointIndex(), leftPalmRotation); return leftPalmRotation; } @@ -1265,8 +1270,8 @@ glm::vec3 Avatar::getUncachedRightPalmPosition() const { return rightPalmPosition; } // avatar didn't have a RightHandMiddle1 joint, fall back on this: - getSkeletonModel()->getJointRotationInWorldFrame(getSkeletonModel()->getRightHandJointIndex(), rightPalmRotation); - getSkeletonModel()->getRightHandPosition(rightPalmPosition); + _skeletonModel->getJointRotationInWorldFrame(_skeletonModel->getRightHandJointIndex(), rightPalmRotation); + _skeletonModel->getRightHandPosition(rightPalmPosition); rightPalmPosition += HAND_TO_PALM_OFFSET * glm::inverse(rightPalmRotation); return rightPalmPosition; } @@ -1274,7 +1279,7 @@ glm::vec3 Avatar::getUncachedRightPalmPosition() const { glm::quat Avatar::getUncachedRightPalmRotation() const { assert(QThread::currentThread() == thread()); // main thread access only glm::quat rightPalmRotation; - getSkeletonModel()->getJointRotationInWorldFrame(getSkeletonModel()->getRightHandJointIndex(), rightPalmRotation); + _skeletonModel->getJointRotationInWorldFrame(_skeletonModel->getRightHandJointIndex(), rightPalmRotation); return rightPalmRotation; } @@ -1291,10 +1296,23 @@ void Avatar::setOrientation(const glm::quat& orientation) { void Avatar::updatePalms() { PerformanceTimer perfTimer("palms"); // update thread-safe caches - _leftPalmRotationCache.set(getUncachedLeftPalmRotation()); - _rightPalmRotationCache.set(getUncachedRightPalmRotation()); - _leftPalmPositionCache.set(getUncachedLeftPalmPosition()); - _rightPalmPositionCache.set(getUncachedRightPalmPosition()); + glm::quat rotation; + int i = _skeletonModel->getLeftHandJointIndex(); + if (_skeletonModel->getJointRotationInWorldFrame(i, rotation)) { + _leftPalmRotationCache.set(rotation); + } + glm::vec3 position; + if (_skeletonModel->getJointPositionInWorldFrame(i, position)) { + _leftPalmPositionCache.set(position); + } + + i = _skeletonModel->getRightHandJointIndex(); + if (_skeletonModel->getJointRotationInWorldFrame(i, rotation)) { + _rightPalmRotationCache.set(rotation); + } + if (_skeletonModel->getJointPositionInWorldFrame(i, position)) { + _rightPalmPositionCache.set(position); + } } void Avatar::setParentID(const QUuid& parentID) { diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 886eafffcf..85e2fceb9b 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -161,6 +161,7 @@ void Rig::destroyAnimGraph() { _internalPoseSet._absolutePoses.clear(); _internalPoseSet._overridePoses.clear(); _internalPoseSet._overrideFlags.clear(); + _numOverrides = 0; } void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset) { @@ -180,6 +181,7 @@ void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOff _internalPoseSet._overrideFlags.clear(); _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + _numOverrides = 0; buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); @@ -207,6 +209,7 @@ void Rig::reset(const FBXGeometry& geometry) { _internalPoseSet._overrideFlags.clear(); _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + _numOverrides = 0; buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); @@ -276,13 +279,17 @@ void Rig::setModelOffset(const glm::mat4& modelOffsetMat) { void Rig::clearJointState(int index) { if (isIndexValid(index)) { - _internalPoseSet._overrideFlags[index] = false; + if (_internalPoseSet._overrideFlags[index]) { + _internalPoseSet._overrideFlags[index] = false; + --_numOverrides; + } _internalPoseSet._overridePoses[index] = _animSkeleton->getRelativeDefaultPose(index); } } void Rig::clearJointStates() { _internalPoseSet._overrideFlags.clear(); + _numOverrides = 0; if (_animSkeleton) { _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints()); _internalPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); @@ -291,7 +298,10 @@ void Rig::clearJointStates() { void Rig::clearJointAnimationPriority(int index) { if (isIndexValid(index)) { - _internalPoseSet._overrideFlags[index] = false; + if (_internalPoseSet._overrideFlags[index]) { + _internalPoseSet._overrideFlags[index] = false; + --_numOverrides; + } _internalPoseSet._overridePoses[index] = _animSkeleton->getRelativeDefaultPose(index); } } @@ -320,7 +330,10 @@ void Rig::setJointTranslation(int index, bool valid, const glm::vec3& translatio if (isIndexValid(index)) { if (valid) { assert(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); - _internalPoseSet._overrideFlags[index] = true; + if (!_internalPoseSet._overrideFlags[index]) { + _internalPoseSet._overrideFlags[index] = true; + ++_numOverrides; + } _internalPoseSet._overridePoses[index].trans = translation; } } @@ -329,7 +342,10 @@ void Rig::setJointTranslation(int index, bool valid, const glm::vec3& translatio void Rig::setJointState(int index, bool valid, const glm::quat& rotation, const glm::vec3& translation, float priority) { if (isIndexValid(index)) { assert(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); - _internalPoseSet._overrideFlags[index] = true; + if (!_internalPoseSet._overrideFlags[index]) { + _internalPoseSet._overrideFlags[index] = true; + ++_numOverrides; + } _internalPoseSet._overridePoses[index].rot = rotation; _internalPoseSet._overridePoses[index].trans = translation; } @@ -339,7 +355,10 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo if (isIndexValid(index)) { if (valid) { ASSERT(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); - _internalPoseSet._overrideFlags[index] = true; + if (!_internalPoseSet._overrideFlags[index]) { + _internalPoseSet._overrideFlags[index] = true; + ++_numOverrides; + } _internalPoseSet._overridePoses[index].rot = rotation; } } @@ -518,7 +537,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos // sine wave LFO var for testing. static float t = 0.0f; - _animVars.set("sine", 2.0f * static_cast(0.5 * sin(t) + 0.5)); + _animVars.set("sine", 2.0f * 0.5f * sinf(t) + 0.5f); float moveForwardAlpha = 0.0f; float moveBackwardAlpha = 0.0f; @@ -884,10 +903,12 @@ void Rig::updateAnimationStateHandlers() { // called on avatar update thread (wh void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xffff00ff, 0); + PerformanceTimer perfTimer("updateAnimations"); setModelOffset(rootTransform); if (_animNode) { + PerformanceTimer perfTimer("handleTriggers"); updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); @@ -903,13 +924,13 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { for (auto& trigger : triggersOut) { _animVars.setTrigger(trigger); } + applyOverridePoses(); } - - applyOverridePoses(); buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); // copy internal poses to external poses { + PerformanceTimer perfTimer("copy"); QWriteLocker writeLock(&_externalPoseSetLock); _externalPoseSet = _internalPoseSet; } @@ -1176,7 +1197,8 @@ bool Rig::getModelRegistrationPoint(glm::vec3& modelRegistrationPointOut) const } void Rig::applyOverridePoses() { - if (!_animSkeleton) { + PerformanceTimer perfTimer("override"); + if (!_animSkeleton || _numOverrides == 0) { return; } @@ -1192,28 +1214,26 @@ void Rig::applyOverridePoses() { } void Rig::buildAbsoluteRigPoses(const AnimPoseVec& relativePoses, AnimPoseVec& absolutePosesOut) { + PerformanceTimer perfTimer("buildAbsolute"); if (!_animSkeleton) { return; } ASSERT(_animSkeleton->getNumJoints() == (int)relativePoses.size()); - // flatten all poses out so they are absolute not relative - absolutePosesOut.resize(relativePoses.size()); - for (int i = 0; i < (int)relativePoses.size(); i++) { - int parentIndex = _animSkeleton->getParentIndex(i); - if (parentIndex == -1) { - absolutePosesOut[i] = relativePoses[i]; - } else { - absolutePosesOut[i] = absolutePosesOut[parentIndex] * relativePoses[i]; + { + absolutePosesOut.resize(relativePoses.size()); + AnimPose geometryToRigTransform(_geometryToRigTransform); + for (int i = 0; i < (int)relativePoses.size(); i++) { + int parentIndex = _animSkeleton->getParentIndex(i); + if (parentIndex == -1) { + // transform all root absolute poses into rig space + absolutePosesOut[i] = geometryToRigTransform * relativePoses[i]; + } else { + absolutePosesOut[i] = absolutePosesOut[parentIndex] * relativePoses[i]; + } } } - - // transform all absolute poses into rig space. - AnimPose geometryToRigTransform(_geometryToRigTransform); - for (int i = 0; i < (int)absolutePosesOut.size(); i++) { - absolutePosesOut[i] = geometryToRigTransform * absolutePosesOut[i]; - } } glm::mat4 Rig::getJointTransform(int jointIndex) const { @@ -1302,11 +1322,10 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { // copy the geometry space parent relative poses into _overridePoses for (int i = 0; i < jointDataVec.size(); i++) { if (overrideFlags[i]) { - _internalPoseSet._overrideFlags[i] = true; - _internalPoseSet._overridePoses[i].scale = Vectors::ONE; - _internalPoseSet._overridePoses[i].rot = rotations[i]; + _internalPoseSet._relativePoses[i].scale = Vectors::ONE; + _internalPoseSet._relativePoses[i].rot = rotations[i]; // scale translations from meters back into geometry units. - _internalPoseSet._overridePoses[i].trans = _invGeometryOffset.scale * translations[i]; + _internalPoseSet._relativePoses[i].trans = _invGeometryOffset.scale * translations[i]; } } } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 151a7ae8e9..aa091fe10c 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -311,6 +311,7 @@ protected: std::map _origRoleAnimations; + int32_t _numOverrides { 0 }; bool _lastEnableInverseKinematics { true }; bool _enableInverseKinematics { true }; From 3f687887b9d1cd2ddb69b8fb5b701bf4def07ddd Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 15 Dec 2016 15:03:20 -0800 Subject: [PATCH 02/68] faster math when unpacking JointData rotations --- libraries/animation/src/Rig.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 85e2fceb9b..dfcace1d77 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1274,14 +1274,13 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._overrideFlags.size()) { // transform all the default poses into rig space. - const AnimPose geometryToRigPose(_geometryToRigTransform); std::vector overrideFlags(_internalPoseSet._overridePoses.size(), false); // start with the default rotations in absolute rig frame std::vector rotations; rotations.reserve(_animSkeleton->getAbsoluteDefaultPoses().size()); for (auto& pose : _animSkeleton->getAbsoluteDefaultPoses()) { - rotations.push_back(geometryToRigPose.rot * pose.rot); + rotations.push_back(pose.rot); } // start translations in relative frame but scaled to meters. @@ -1294,12 +1293,14 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { ASSERT(overrideFlags.size() == rotations.size()); // copy over rotations from the jointDataVec, which is also in absolute rig frame + const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); for (int i = 0; i < jointDataVec.size(); i++) { if (isIndexValid(i)) { const JointData& data = jointDataVec.at(i); if (data.rotationSet) { overrideFlags[i] = true; - rotations[i] = data.rotation; + // JointData rotations are in rig frame + rotations[i] = rigToGeometryRot * data.rotation; } if (data.translationSet) { overrideFlags[i] = true; @@ -1310,12 +1311,6 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { ASSERT(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); - // convert resulting rotations into geometry space. - const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); - for (auto& rot : rotations) { - rot = rigToGeometryRot * rot; - } - // convert all rotations from absolute to parent relative. _animSkeleton->convertAbsoluteRotationsToRelative(rotations); From b937eff582eeca1372b17f43e3d746dcc6cd002f Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 15 Dec 2016 16:03:44 -0800 Subject: [PATCH 03/68] more faster math copying JointData --- libraries/animation/src/Rig.cpp | 60 +++++++++++---------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index dfcace1d77..c6aae824e4 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1271,56 +1271,36 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { void Rig::copyJointsFromJointData(const QVector& jointDataVec) { PerformanceTimer perfTimer("copyJoints"); - if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._overrideFlags.size()) { - - // transform all the default poses into rig space. - std::vector overrideFlags(_internalPoseSet._overridePoses.size(), false); - - // start with the default rotations in absolute rig frame + if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._relativePoses.size()) { + // make a vector of rotations in absolute-geometry-frame + const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses(); std::vector rotations; rotations.reserve(_animSkeleton->getAbsoluteDefaultPoses().size()); - for (auto& pose : _animSkeleton->getAbsoluteDefaultPoses()) { - rotations.push_back(pose.rot); - } - - // start translations in relative frame but scaled to meters. - std::vector translations; - translations.reserve(_animSkeleton->getRelativeDefaultPoses().size()); - for (auto& pose : _animSkeleton->getRelativeDefaultPoses()) { - translations.push_back(_geometryOffset.scale * pose.trans); - } - - ASSERT(overrideFlags.size() == rotations.size()); - - // copy over rotations from the jointDataVec, which is also in absolute rig frame const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); for (int i = 0; i < jointDataVec.size(); i++) { - if (isIndexValid(i)) { - const JointData& data = jointDataVec.at(i); - if (data.rotationSet) { - overrideFlags[i] = true; - // JointData rotations are in rig frame - rotations[i] = rigToGeometryRot * data.rotation; - } - if (data.translationSet) { - overrideFlags[i] = true; - translations[i] = data.translation; - } + const JointData& data = jointDataVec.at(i); + if (data.rotationSet) { + // JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame + rotations.push_back(rigToGeometryRot * data.rotation); + } else { + rotations.push_back(absoluteDefaultPoses[i].rot); } } - ASSERT(_internalPoseSet._overrideFlags.size() == _internalPoseSet._overridePoses.size()); - - // convert all rotations from absolute to parent relative. + // convert rotations from absolute to parent relative. _animSkeleton->convertAbsoluteRotationsToRelative(rotations); - // copy the geometry space parent relative poses into _overridePoses + // store new relative poses + const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses(); for (int i = 0; i < jointDataVec.size(); i++) { - if (overrideFlags[i]) { - _internalPoseSet._relativePoses[i].scale = Vectors::ONE; - _internalPoseSet._relativePoses[i].rot = rotations[i]; - // scale translations from meters back into geometry units. - _internalPoseSet._relativePoses[i].trans = _invGeometryOffset.scale * translations[i]; + const JointData& data = jointDataVec.at(i); + _internalPoseSet._relativePoses[i].scale = Vectors::ONE; + _internalPoseSet._relativePoses[i].rot = rotations[i]; + if (data.translationSet) { + // JointData translations are in scaled relative-frame so we scale back to regular relative-frame + _internalPoseSet._relativePoses[i].trans = _invGeometryOffset.scale * data.translation; + } else { + _internalPoseSet._relativePoses[i].trans = relativeDefaultPoses[i].trans; } } } From 383064999051202ff6e61258278bc08ec7e8bfd8 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 16 Dec 2016 08:46:11 -0800 Subject: [PATCH 04/68] remove debugging and profiling --- interface/src/avatar/Avatar.cpp | 3 --- libraries/animation/src/Rig.cpp | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 908b3def8d..a79d2372c5 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -645,8 +645,6 @@ glm::quat Avatar::computeRotationFromBodyToWorldUp(float proportion) const { } void Avatar::fixupModelsInScene() { -#define ADEBUG -#ifdef ADEBUG _attachmentsToDelete.clear(); // check to see if when we added our models to the scene they were ready, if they were not ready, then @@ -670,7 +668,6 @@ void Avatar::fixupModelsInScene() { _attachmentsToDelete.insert(_attachmentsToDelete.end(), _attachmentsToRemove.begin(), _attachmentsToRemove.end()); _attachmentsToRemove.clear(); scene->enqueuePendingChanges(pendingChanges); -#endif // ADEBUG } bool Avatar::shouldRenderHead(const RenderArgs* renderArgs) const { diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index c6aae824e4..b6bb8ccc8f 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -929,11 +929,8 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); // copy internal poses to external poses - { - PerformanceTimer perfTimer("copy"); - QWriteLocker writeLock(&_externalPoseSetLock); - _externalPoseSet = _internalPoseSet; - } + QWriteLocker writeLock(&_externalPoseSetLock); + _externalPoseSet = _internalPoseSet; } void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, From 2209c0ebbac75a665350ce67eb2f63dd260535a9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 16 Dec 2016 13:10:30 -0800 Subject: [PATCH 05/68] remove debug hook for dumping stats to logs --- interface/src/Application.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e32e9e90b1..d92ba53f6e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2572,7 +2572,6 @@ bool Application::eventFilter(QObject* object, QEvent* event) { } static bool _altPressed{ false }; -static bool dumpAllTimerRecords { false }; // adebug hack void Application::keyPressEvent(QKeyEvent* event) { _altPressed = event->key() == Qt::Key_Alt; @@ -2663,8 +2662,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_F: { - //_physicsEngine->dumpNextStats(); - dumpAllTimerRecords = true; // adebug hack + _physicsEngine->dumpNextStats(); break; } @@ -3410,11 +3408,6 @@ void Application::idle(float nsecsElapsed) { } _overlayConductor.update(secondsSinceLastUpdate); - // adebug hack - if (dumpAllTimerRecords) { - PerformanceTimer::dumpAllTimerRecords(); - dumpAllTimerRecords = false; - } } void Application::setLowVelocityFilter(bool lowVelocityFilter) { From 0b0c3f0be4cee9aeea4a78f2239bd247082903fb Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 20 Dec 2016 16:43:50 -0800 Subject: [PATCH 06/68] cleanup some if-logic --- libraries/render-utils/src/Model.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 14c80faa96..436574a1ff 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -234,17 +234,19 @@ void Model::updateRenderItems() { render::PendingChanges pendingChanges; foreach (auto itemID, self->_modelMeshRenderItems.keys()) { pendingChanges.updateItem(itemID, [modelTransform, modelMeshOffset, deleteGeometryCounter](ModelMeshPartPayload& data) { - if (!data.hasStartedFade() && data._model && data._model->isLoaded() && data._model->getGeometry()->areTexturesLoaded()) { - data.startFade(); - } - // Ensure the model geometry was not reset between frames - if (data._model && data._model->isLoaded() && deleteGeometryCounter == data._model->_deleteGeometryCounter) { - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - data._model->updateClusterMatrices(modelTransform.getTranslation(), modelTransform.getRotation()); + if (data._model && data._model->isLoaded()) { + if (!data.hasStartedFade() && data._model->getGeometry()->areTexturesLoaded()) { + data.startFade(); + } + // Ensure the model geometry was not reset between frames + if (deleteGeometryCounter == data._model->_deleteGeometryCounter) { + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + data._model->updateClusterMatrices(modelTransform.getTranslation(), modelTransform.getRotation()); - // update the model transform and bounding box for this render item. - const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); - data.updateTransformForSkinnedMesh(modelTransform, modelMeshOffset, state.clusterMatrices); + // update the model transform and bounding box for this render item. + const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); + data.updateTransformForSkinnedMesh(modelTransform, modelMeshOffset, state.clusterMatrices); + } } }); } From e6a20102d4653e17ad9b6383a31bc6253dc69007 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 21 Dec 2016 11:31:34 -0800 Subject: [PATCH 07/68] debug window enhancements --- interface/src/Menu.cpp | 9 ++++++++ scripts/developer/debugging/debugWindow.js | 24 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index b20343c439..4819220400 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -704,6 +704,15 @@ Menu::Menu() { addActionToQMenuAndActionHash(developerMenu, MenuOption::Log, Qt::CTRL | Qt::SHIFT | Qt::Key_L, qApp, SLOT(toggleLogDialog())); + action = addActionToQMenuAndActionHash(developerMenu, "Script Log (HMD friendly)..."); + connect(action, &QAction::triggered, [] { + auto scriptEngines = DependencyManager::get(); + QUrl defaultScriptsLoc = defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); + scriptEngines->loadScript(defaultScriptsLoc.toString()); + }); + + // Developer > Stats addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats); diff --git a/scripts/developer/debugging/debugWindow.js b/scripts/developer/debugging/debugWindow.js index fb9f3f4847..30a050e667 100644 --- a/scripts/developer/debugging/debugWindow.js +++ b/scripts/developer/debugging/debugWindow.js @@ -8,6 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +(function() { // BEGIN LOCAL_SCOPE // Set up the qml ui var qml = Script.resolvePath('debugWindow.qml'); @@ -19,18 +20,33 @@ var window = new OverlayWindow({ window.setPosition(25, 50); window.closed.connect(function() { Script.stop(); }); +var getFormattedDate = function() { + var date = new Date(); + return date.getMonth() + "/" + date.getDate() + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); +}; + +var sendToLogWindow = function(type, message, scriptFileName) { + var typeFormatted = ""; + if (type) { + typeFormatted = type + " - "; + } + window.sendToQml("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message); +}; + ScriptDiscoveryService.printedMessage.connect(function(message, scriptFileName) { - window.sendToQml("[" + scriptFileName + "] " + message); + sendToLogWindow("", message, scriptFileName); }); ScriptDiscoveryService.warningMessage.connect(function(message, scriptFileName) { - window.sendToQml("[" + scriptFileName + "] WARNING - " + message); + sendToLogWindow("WARNING", message, scriptFileName); }); ScriptDiscoveryService.errorMessage.connect(function(message, scriptFileName) { - window.sendToQml("[" + scriptFileName + "] ERROR - " + message); + sendToLogWindow("ERROR", message, scriptFileName); }); ScriptDiscoveryService.infoMessage.connect(function(message, scriptFileName) { - window.sendToQml("[" + scriptFileName + "] INFO - " + message); + sendToLogWindow("INFO", message, scriptFileName); }); + +}()); // END LOCAL_SCOPE \ No newline at end of file From 8e7b062aa2b15b7c995f30259776fe2a1c993586 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Wed, 21 Dec 2016 11:52:24 -0800 Subject: [PATCH 08/68] cleanup avatar data to include better documentation --- libraries/avatars/src/AvatarData.cpp | 115 +++++++++++++++------------ 1 file changed, 64 insertions(+), 51 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index d77f70fbe9..f8805d3fc4 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -50,9 +50,20 @@ const glm::vec3 DEFAULT_LOCAL_AABOX_SCALE(1.0f); const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData"; namespace AvatarDataPacket { + // NOTE: AvatarDataPackets start with a uint16_t sequence number that is not reflected in the Header structure. PACKED_BEGIN struct Header { + uint8_t packetStateFlags; // state flags, currently used to indicate if the packet is a minimal or fuller packet + } PACKED_END; + const size_t HEADER_SIZE = 1; + + PACKED_BEGIN struct MinimalAvatarInfo { + float globalPosition[3]; // avatar's position + } PACKED_END; + const size_t MINIMAL_AVATAR_INFO_SIZE = 12; + + PACKED_BEGIN struct AvatarInfo { float position[3]; // skeletal model's position float globalPosition[3]; // avatar's position float globalBoundingBoxCorner[3]; // global position of the lowest corner of the avatar's bounding box @@ -65,16 +76,16 @@ namespace AvatarDataPacket { float sensorToWorldTrans[3]; // fourth column of sensor to world matrix uint8_t flags; } PACKED_END; - const size_t HEADER_SIZE = 81; + const size_t AVATAR_INFO_SIZE = 81; - // only present if HAS_REFERENTIAL flag is set in header.flags + // only present if HAS_REFERENTIAL flag is set in AvatarInfo.flags PACKED_BEGIN struct ParentInfo { uint8_t parentUUID[16]; // rfc 4122 encoded uint16_t parentJointIndex; } PACKED_END; const size_t PARENT_INFO_SIZE = 18; - // only present if IS_FACESHIFT_CONNECTED flag is set in header.flags + // only present if IS_FACESHIFT_CONNECTED flag is set in AvatarInfo.flags PACKED_BEGIN struct FaceTrackerInfo { float leftEyeBlink; float rightEyeBlink; @@ -124,6 +135,8 @@ AvatarData::AvatarData() : setBodyRoll(0.0f); ASSERT(sizeof(AvatarDataPacket::Header) == AvatarDataPacket::HEADER_SIZE); + ASSERT(sizeof(AvatarDataPacket::MinimalAvatarInfo) == AvatarDataPacket::MINIMAL_AVATAR_INFO_SIZE); + ASSERT(sizeof(AvatarDataPacket::AvatarInfo) == AvatarDataPacket::AVATAR_INFO_SIZE); ASSERT(sizeof(AvatarDataPacket::ParentInfo) == AvatarDataPacket::PARENT_INFO_SIZE); ASSERT(sizeof(AvatarDataPacket::FaceTrackerInfo) == AvatarDataPacket::FACE_TRACKER_INFO_SIZE); } @@ -132,9 +145,9 @@ AvatarData::~AvatarData() { delete _headData; } -// We cannot have a file-level variable (const or otherwise) in the header if it uses PathUtils, because that references Application, which will not yet initialized. +// We cannot have a file-level variable (const or otherwise) in the AvatarInfo if it uses PathUtils, because that references Application, which will not yet initialized. // Thus we have a static class getter, referencing a static class var. -QUrl AvatarData::_defaultFullAvatarModelUrl = {}; // In C++, if this initialization were in the header, every file would have it's own copy, even for class vars. +QUrl AvatarData::_defaultFullAvatarModelUrl = {}; // In C++, if this initialization were in the AvatarInfo, every file would have it's own copy, even for class vars. const QUrl& AvatarData::defaultFullAvatarModelUrl() { if (_defaultFullAvatarModelUrl.isEmpty()) { _defaultFullAvatarModelUrl = QUrl::fromLocalFile(PathUtils::resourcesPath() + "meshes/defaultAvatar_full.fst"); @@ -216,56 +229,56 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail) { memcpy(destinationBuffer, &_globalPosition, sizeof(_globalPosition)); destinationBuffer += sizeof(_globalPosition); } else { - auto header = reinterpret_cast(destinationBuffer); - header->position[0] = getLocalPosition().x; - header->position[1] = getLocalPosition().y; - header->position[2] = getLocalPosition().z; - header->globalPosition[0] = _globalPosition.x; - header->globalPosition[1] = _globalPosition.y; - header->globalPosition[2] = _globalPosition.z; - header->globalBoundingBoxCorner[0] = getPosition().x - _globalBoundingBoxCorner.x; - header->globalBoundingBoxCorner[1] = getPosition().y - _globalBoundingBoxCorner.y; - header->globalBoundingBoxCorner[2] = getPosition().z - _globalBoundingBoxCorner.z; + auto avatarInfo = reinterpret_cast(destinationBuffer); + avatarInfo->position[0] = getLocalPosition().x; + avatarInfo->position[1] = getLocalPosition().y; + avatarInfo->position[2] = getLocalPosition().z; + avatarInfo->globalPosition[0] = _globalPosition.x; + avatarInfo->globalPosition[1] = _globalPosition.y; + avatarInfo->globalPosition[2] = _globalPosition.z; + avatarInfo->globalBoundingBoxCorner[0] = getPosition().x - _globalBoundingBoxCorner.x; + avatarInfo->globalBoundingBoxCorner[1] = getPosition().y - _globalBoundingBoxCorner.y; + avatarInfo->globalBoundingBoxCorner[2] = getPosition().z - _globalBoundingBoxCorner.z; glm::vec3 bodyEulerAngles = glm::degrees(safeEulerAngles(getLocalOrientation())); - packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 0), bodyEulerAngles.y); - packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 1), bodyEulerAngles.x); - packFloatAngleToTwoByte((uint8_t*)(header->localOrientation + 2), bodyEulerAngles.z); - packFloatRatioToTwoByte((uint8_t*)(&header->scale), getDomainLimitedScale()); - header->lookAtPosition[0] = _headData->_lookAtPosition.x; - header->lookAtPosition[1] = _headData->_lookAtPosition.y; - header->lookAtPosition[2] = _headData->_lookAtPosition.z; - header->audioLoudness = _headData->_audioLoudness; + packFloatAngleToTwoByte((uint8_t*)(avatarInfo->localOrientation + 0), bodyEulerAngles.y); + packFloatAngleToTwoByte((uint8_t*)(avatarInfo->localOrientation + 1), bodyEulerAngles.x); + packFloatAngleToTwoByte((uint8_t*)(avatarInfo->localOrientation + 2), bodyEulerAngles.z); + packFloatRatioToTwoByte((uint8_t*)(&avatarInfo->scale), getDomainLimitedScale()); + avatarInfo->lookAtPosition[0] = _headData->_lookAtPosition.x; + avatarInfo->lookAtPosition[1] = _headData->_lookAtPosition.y; + avatarInfo->lookAtPosition[2] = _headData->_lookAtPosition.z; + avatarInfo->audioLoudness = _headData->_audioLoudness; glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); - packOrientationQuatToSixBytes(header->sensorToWorldQuat, glmExtractRotation(sensorToWorldMatrix)); + packOrientationQuatToSixBytes(avatarInfo->sensorToWorldQuat, glmExtractRotation(sensorToWorldMatrix)); glm::vec3 scale = extractScale(sensorToWorldMatrix); - packFloatScalarToSignedTwoByteFixed((uint8_t*)&header->sensorToWorldScale, scale.x, SENSOR_TO_WORLD_SCALE_RADIX); - header->sensorToWorldTrans[0] = sensorToWorldMatrix[3][0]; - header->sensorToWorldTrans[1] = sensorToWorldMatrix[3][1]; - header->sensorToWorldTrans[2] = sensorToWorldMatrix[3][2]; + packFloatScalarToSignedTwoByteFixed((uint8_t*)&avatarInfo->sensorToWorldScale, scale.x, SENSOR_TO_WORLD_SCALE_RADIX); + avatarInfo->sensorToWorldTrans[0] = sensorToWorldMatrix[3][0]; + avatarInfo->sensorToWorldTrans[1] = sensorToWorldMatrix[3][1]; + avatarInfo->sensorToWorldTrans[2] = sensorToWorldMatrix[3][2]; - setSemiNibbleAt(header->flags, KEY_STATE_START_BIT, _keyState); + setSemiNibbleAt(avatarInfo->flags, KEY_STATE_START_BIT, _keyState); // hand state bool isFingerPointing = _handState & IS_FINGER_POINTING_FLAG; - setSemiNibbleAt(header->flags, HAND_STATE_START_BIT, _handState & ~IS_FINGER_POINTING_FLAG); + setSemiNibbleAt(avatarInfo->flags, HAND_STATE_START_BIT, _handState & ~IS_FINGER_POINTING_FLAG); if (isFingerPointing) { - setAtBit(header->flags, HAND_STATE_FINGER_POINTING_BIT); + setAtBit(avatarInfo->flags, HAND_STATE_FINGER_POINTING_BIT); } // faceshift state if (_headData->_isFaceTrackerConnected) { - setAtBit(header->flags, IS_FACESHIFT_CONNECTED); + setAtBit(avatarInfo->flags, IS_FACESHIFT_CONNECTED); } // eye tracker state if (_headData->_isEyeTrackerConnected) { - setAtBit(header->flags, IS_EYE_TRACKER_CONNECTED); + setAtBit(avatarInfo->flags, IS_EYE_TRACKER_CONNECTED); } // referential state QUuid parentID = getParentID(); if (!parentID.isNull()) { - setAtBit(header->flags, HAS_REFERENTIAL); + setAtBit(avatarInfo->flags, HAS_REFERENTIAL); } - destinationBuffer += sizeof(AvatarDataPacket::Header); + destinationBuffer += sizeof(AvatarDataPacket::AvatarInfo); if (!parentID.isNull()) { auto parentInfo = reinterpret_cast(destinationBuffer); @@ -510,13 +523,13 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { quint64 now = usecTimestampNow(); - PACKET_READ_CHECK(Header, sizeof(AvatarDataPacket::Header)); - auto header = reinterpret_cast(sourceBuffer); - sourceBuffer += sizeof(AvatarDataPacket::Header); + PACKET_READ_CHECK(AvatarInfo, sizeof(AvatarDataPacket::AvatarInfo)); + auto avatarInfo = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(AvatarDataPacket::AvatarInfo); - glm::vec3 position = glm::vec3(header->position[0], header->position[1], header->position[2]); - _globalPosition = glm::vec3(header->globalPosition[0], header->globalPosition[1], header->globalPosition[2]); - _globalBoundingBoxCorner = glm::vec3(header->globalBoundingBoxCorner[0], header->globalBoundingBoxCorner[1], header->globalBoundingBoxCorner[2]); + glm::vec3 position = glm::vec3(avatarInfo->position[0], avatarInfo->position[1], avatarInfo->position[2]); + _globalPosition = glm::vec3(avatarInfo->globalPosition[0], avatarInfo->globalPosition[1], avatarInfo->globalPosition[2]); + _globalBoundingBoxCorner = glm::vec3(avatarInfo->globalBoundingBoxCorner[0], avatarInfo->globalBoundingBoxCorner[1], avatarInfo->globalBoundingBoxCorner[2]); if (isNaN(position)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: position NaN, uuid " << getSessionUUID(); @@ -526,9 +539,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { setLocalPosition(position); float pitch, yaw, roll; - unpackFloatAngleFromTwoByte(header->localOrientation + 0, &yaw); - unpackFloatAngleFromTwoByte(header->localOrientation + 1, &pitch); - unpackFloatAngleFromTwoByte(header->localOrientation + 2, &roll); + unpackFloatAngleFromTwoByte(avatarInfo->localOrientation + 0, &yaw); + unpackFloatAngleFromTwoByte(avatarInfo->localOrientation + 1, &pitch); + unpackFloatAngleFromTwoByte(avatarInfo->localOrientation + 2, &roll); if (isNaN(yaw) || isNaN(pitch) || isNaN(roll)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: localOriention is NaN, uuid " << getSessionUUID(); @@ -545,7 +558,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } float scale; - unpackFloatRatioFromTwoByte((uint8_t*)&header->scale, scale); + unpackFloatRatioFromTwoByte((uint8_t*)&avatarInfo->scale, scale); if (isNaN(scale)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: scale NaN, uuid " << getSessionUUID(); @@ -554,7 +567,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } setTargetScale(scale); - glm::vec3 lookAt = glm::vec3(header->lookAtPosition[0], header->lookAtPosition[1], header->lookAtPosition[2]); + glm::vec3 lookAt = glm::vec3(avatarInfo->lookAtPosition[0], avatarInfo->lookAtPosition[1], avatarInfo->lookAtPosition[2]); if (isNaN(lookAt)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: lookAtPosition is NaN, uuid " << getSessionUUID(); @@ -563,7 +576,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } _headData->_lookAtPosition = lookAt; - float audioLoudness = header->audioLoudness; + float audioLoudness = avatarInfo->audioLoudness; if (isNaN(audioLoudness)) { if (shouldLogError(now)) { qCWarning(avatars) << "Discard AvatarData packet: audioLoudness is NaN, uuid " << getSessionUUID(); @@ -573,16 +586,16 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _headData->_audioLoudness = audioLoudness; glm::quat sensorToWorldQuat; - unpackOrientationQuatFromSixBytes(header->sensorToWorldQuat, sensorToWorldQuat); + unpackOrientationQuatFromSixBytes(avatarInfo->sensorToWorldQuat, sensorToWorldQuat); float sensorToWorldScale; - unpackFloatScalarFromSignedTwoByteFixed((int16_t*)&header->sensorToWorldScale, &sensorToWorldScale, SENSOR_TO_WORLD_SCALE_RADIX); - glm::vec3 sensorToWorldTrans(header->sensorToWorldTrans[0], header->sensorToWorldTrans[1], header->sensorToWorldTrans[2]); + unpackFloatScalarFromSignedTwoByteFixed((int16_t*)&avatarInfo->sensorToWorldScale, &sensorToWorldScale, SENSOR_TO_WORLD_SCALE_RADIX); + glm::vec3 sensorToWorldTrans(avatarInfo->sensorToWorldTrans[0], avatarInfo->sensorToWorldTrans[1], avatarInfo->sensorToWorldTrans[2]); glm::mat4 sensorToWorldMatrix = createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), sensorToWorldQuat, sensorToWorldTrans); _sensorToWorldMatrixCache.set(sensorToWorldMatrix); { // bitFlags and face data - uint8_t bitItems = header->flags; + uint8_t bitItems = avatarInfo->flags; // key state, stored as a semi-nibble in the bitItems _keyState = (KeyState)getSemiNibbleAt(bitItems, KEY_STATE_START_BIT); From 59962286226c991c71282b071fb274a290dd53c0 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 19 Dec 2016 13:06:48 -0800 Subject: [PATCH 09/68] Starting off strong. --- interface/resources/qml/hifi/NameCard.qml | 16 +- interface/resources/qml/hifi/Pal.qml | 217 +++++++++++++--------- 2 files changed, 138 insertions(+), 95 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index ffb42d2fff..48f0f61d03 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -20,17 +20,27 @@ Column { property int displayTextHeight: 18; property int usernameTextHeight: 12; + RalewaySemiBold { + // Properties text: parent.displayName; - size: parent.displayTextHeight; elide: Text.ElideRight; + // Size width: parent.width; + // Text Size + size: parent.displayTextHeight; + // Text Positioning + verticalAlignment: Text.AlignVCenter; } RalewayLight { - visible: parent.displayName; + // Properties text: parent.userName; - size: parent.usernameTextHeight; elide: Text.ElideRight; + visible: parent.displayName; + // Size + size: parent.usernameTextHeight; width: parent.width; + // Text Positioning + verticalAlignment: Text.AlignVCenter; } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 7a8dc4722e..060a14b1dd 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -31,13 +31,133 @@ import QtQuick.Controls 1.4 Rectangle { id: pal; - property int keepFromHorizontalScroll: 1; - width: parent.width - keepFromHorizontalScroll; + // Size + width: parent.width; height: parent.height; - - property int nameWidth: width/2; - property int actionWidth: nameWidth / (table.columnCount - 1); + // Properties + property int myCardHeight: 75; property int rowHeight: 50; + property int nameCardWidth: width*3/5; + property int separatorColWidth: 30; + property int actionWidth: (width - nameCardWidth - separatorColWidth) / (table.columnCount - 2); // "-2" for Name and Separator cols + + Column { + // This NameCard refers to the current user's NameCard (the one above the table) + NameCard { + id: myCard; + // Properties + displayName: myData.displayName; + userName: myData.userName; + // Size + width: nameCardWidth; + height: myCardHeight; + // Positioning + anchors.top: pal.top; + anchors.left: parent.left; + // Margins + anchors.leftMargin: 10; + } + // This TableView refers to the table (below the current user's NameCard) + TableView { + id: table; + // Size + height: pal.height - myCard.height; + width: pal.width; + // Positioning + anchors.top: myCard.bottom; + // Properties + frameVisible: false; + sortIndicatorVisible: true; + onSortIndicatorColumnChanged: sortModel(); + onSortIndicatorOrderChanged: sortModel(); + + TableViewColumn { + role: "displayName"; + title: "Name"; + width: nameCardWidth + } + TableViewColumn { + role: "ignore"; + title: "Ignore" + width: actionWidth + } + TableViewColumn { + title: ""; + width: separatorColWidth + } + TableViewColumn { + visible: iAmAdmin; + role: "mute"; + title: "Mute"; + width: actionWidth + } + TableViewColumn { + visible: iAmAdmin; + role: "kick"; + title: "Ban" + width: actionWidth + } + model: userModel; + + // This Rectangle refers to each Row in the table. + rowDelegate: Rectangle { // The only way I know to specify a row height. + height: rowHeight; + // The rest of this is cargo-culted to restore the default styling + SystemPalette { + id: myPalette; + colorGroup: SystemPalette.Active + } + color: { + var baseColor = styleData.alternate?myPalette.alternateBase:myPalette.base + return styleData.selected?myPalette.highlight:baseColor + } + } + + // This Item refers to the contents of each Cell + itemDelegate: Item { + id: itemCell; + property bool isCheckBox: typeof(styleData.value) === 'boolean'; + // This NameCard refers to the cell that contains an avatar's + // DisplayName and UserName + NameCard { + id: nameCard; + // Properties + displayName: styleData.value; + userName: model.userName; + visible: !isCheckBox; + // Size + width: nameCardWidth; + // Positioning + anchors.left: parent.left; + anchors.verticalCenter: parent.verticalCenter; + // Margins + anchors.leftMargin: 10; + } + // This Rectangle refers to the cells that contain the action buttons + Rectangle { + radius: itemCell.height / 4; + visible: isCheckBox; + color: styleData.value ? "green" : "red"; + anchors.fill: parent; + MouseArea { + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + hoverEnabled: true; + onClicked: { + var newValue = !model[styleData.role]; + var datum = userData[model.userIndex]; + datum[styleData.role] = model[styleData.role] = newValue; + Users[styleData.role](model.sessionId); + // Just for now, while we cannot undo things: + userData.splice(model.userIndex, 1); + sortModel(); + } + } + } + } + } + } + property var userData: []; property var myData: ({displayName: "", userName: ""}); // valid dummy until set property bool iAmAdmin: false; @@ -135,91 +255,4 @@ Rectangle { target: table.selection onSelectionChanged: pal.noticeSelection() } - - Column { - NameCard { - id: myCard; - width: nameWidth; - displayName: myData.displayName; - userName: myData.userName; - } - TableView { - id: table; - TableViewColumn { - role: "displayName"; - title: "Name"; - width: nameWidth - } - TableViewColumn { - role: "ignore"; - title: "Ignore" - width: actionWidth - } - TableViewColumn { - title: ""; - width: actionWidth - } - TableViewColumn { - visible: iAmAdmin; - role: "mute"; - title: "Mute"; - width: actionWidth - } - TableViewColumn { - visible: iAmAdmin; - role: "kick"; - title: "Ban" - width: actionWidth - } - model: userModel; - rowDelegate: Rectangle { // The only way I know to specify a row height. - height: rowHeight; - // The rest of this is cargo-culted to restore the default styling - SystemPalette { - id: myPalette; - colorGroup: SystemPalette.Active - } - color: { - var baseColor = styleData.alternate?myPalette.alternateBase:myPalette.base - return styleData.selected?myPalette.highlight:baseColor - } - } - itemDelegate: Item { - id: itemCell; - property bool isCheckBox: typeof(styleData.value) === 'boolean'; - NameCard { - id: nameCard; - visible: !isCheckBox; - width: nameWidth; - displayName: styleData.value; - userName: model.userName; - } - Rectangle { - radius: itemCell.height / 4; - visible: isCheckBox; - color: styleData.value ? "green" : "red"; - anchors.fill: parent; - MouseArea { - anchors.fill: parent; - acceptedButtons: Qt.LeftButton; - hoverEnabled: true; - onClicked: { - var newValue = !model[styleData.role]; - var datum = userData[model.userIndex]; - datum[styleData.role] = model[styleData.role] = newValue; - Users[styleData.role](model.sessionId); - // Just for now, while we cannot undo things: - userData.splice(model.userIndex, 1); - sortModel(); - } - } - } - } - height: pal.height - myCard.height; - width: pal.width; - sortIndicatorVisible: true; - onSortIndicatorColumnChanged: sortModel(); - onSortIndicatorOrderChanged: sortModel(); - } - } } From 46c787512ef4083ec307ec9a92768457e16e46f1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 19 Dec 2016 14:05:55 -0800 Subject: [PATCH 10/68] Working --- interface/resources/qml/hifi/NameCard.qml | 4 +- interface/resources/qml/hifi/Pal.qml | 195 +++++++++++----------- 2 files changed, 103 insertions(+), 96 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 48f0f61d03..ef3afa22ff 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -15,11 +15,11 @@ import "../styles-uit" Column { + // Properties property string displayName: ""; property string userName: ""; property int displayTextHeight: 18; - property int usernameTextHeight: 12; - + property int usernameTextHeight: 12 RalewaySemiBold { // Properties diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 060a14b1dd..b0710a1dda 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -41,7 +41,15 @@ Rectangle { property int separatorColWidth: 30; property int actionWidth: (width - nameCardWidth - separatorColWidth) / (table.columnCount - 2); // "-2" for Name and Separator cols - Column { + // This contains the current user's NameCard and will contain other information in the future + Item { + id: myInfo; + // Size + width: pal.width; + height: myCardHeight; + // Positioning + anchors.top: pal.top; + anchors.left: pal.left; // This NameCard refers to the current user's NameCard (the one above the table) NameCard { id: myCard; @@ -49,109 +57,108 @@ Rectangle { displayName: myData.displayName; userName: myData.userName; // Size - width: nameCardWidth; - height: myCardHeight; + width: nameCardWidth - anchors.leftMargin; // Positioning - anchors.top: pal.top; anchors.left: parent.left; + anchors.verticalCenter: parent.verticalCenter; // Margins anchors.leftMargin: 10; } - // This TableView refers to the table (below the current user's NameCard) - TableView { - id: table; - // Size - height: pal.height - myCard.height; - width: pal.width; - // Positioning - anchors.top: myCard.bottom; - // Properties - frameVisible: false; - sortIndicatorVisible: true; - onSortIndicatorColumnChanged: sortModel(); - onSortIndicatorOrderChanged: sortModel(); + } + // This TableView refers to the table (below the current user's NameCard) + TableView { + id: table; + // Size + height: pal.height - myInfo.height; + width: pal.width; + // Positioning + anchors.top: myInfo.bottom; + // Properties + frameVisible: false; + sortIndicatorVisible: true; + onSortIndicatorColumnChanged: sortModel(); + onSortIndicatorOrderChanged: sortModel(); - TableViewColumn { - role: "displayName"; - title: "Name"; - width: nameCardWidth - } - TableViewColumn { - role: "ignore"; - title: "Ignore" - width: actionWidth - } - TableViewColumn { - title: ""; - width: separatorColWidth - } - TableViewColumn { - visible: iAmAdmin; - role: "mute"; - title: "Mute"; - width: actionWidth - } - TableViewColumn { - visible: iAmAdmin; - role: "kick"; - title: "Ban" - width: actionWidth - } - model: userModel; + TableViewColumn { + role: "displayName"; + title: "Name"; + width: nameCardWidth + } + TableViewColumn { + role: "ignore"; + title: "Ignore" + width: actionWidth + } + TableViewColumn { + title: ""; + width: separatorColWidth + } + TableViewColumn { + visible: iAmAdmin; + role: "mute"; + title: "Mute"; + width: actionWidth + } + TableViewColumn { + visible: iAmAdmin; + role: "kick"; + title: "Ban" + width: actionWidth + } + model: userModel; - // This Rectangle refers to each Row in the table. - rowDelegate: Rectangle { // The only way I know to specify a row height. - height: rowHeight; - // The rest of this is cargo-culted to restore the default styling - SystemPalette { - id: myPalette; - colorGroup: SystemPalette.Active - } - color: { - var baseColor = styleData.alternate?myPalette.alternateBase:myPalette.base - return styleData.selected?myPalette.highlight:baseColor - } + // This Rectangle refers to each Row in the table. + rowDelegate: Rectangle { // The only way I know to specify a row height. + height: rowHeight; + // The rest of this is cargo-culted to restore the default styling + SystemPalette { + id: myPalette; + colorGroup: SystemPalette.Active } + color: { + var baseColor = styleData.alternate?myPalette.alternateBase:myPalette.base + return styleData.selected?myPalette.highlight:baseColor + } + } - // This Item refers to the contents of each Cell - itemDelegate: Item { - id: itemCell; - property bool isCheckBox: typeof(styleData.value) === 'boolean'; - // This NameCard refers to the cell that contains an avatar's - // DisplayName and UserName - NameCard { - id: nameCard; - // Properties - displayName: styleData.value; - userName: model.userName; - visible: !isCheckBox; - // Size - width: nameCardWidth; - // Positioning - anchors.left: parent.left; - anchors.verticalCenter: parent.verticalCenter; - // Margins - anchors.leftMargin: 10; - } - // This Rectangle refers to the cells that contain the action buttons - Rectangle { - radius: itemCell.height / 4; - visible: isCheckBox; - color: styleData.value ? "green" : "red"; + // This Item refers to the contents of each Cell + itemDelegate: Item { + id: itemCell; + property bool isCheckBox: typeof(styleData.value) === 'boolean'; + // This NameCard refers to the cell that contains an avatar's + // DisplayName and UserName + NameCard { + id: nameCard; + // Properties + displayName: styleData.value; + userName: model.userName; + visible: !isCheckBox; + // Size + width: nameCardWidth - anchors.leftMargin; + // Positioning + anchors.left: parent.left; + anchors.verticalCenter: parent.verticalCenter; + // Margins + anchors.leftMargin: 10; + } + // This Rectangle refers to the cells that contain the action buttons + Rectangle { + radius: itemCell.height / 4; + visible: isCheckBox; + color: styleData.value ? "green" : "red"; + anchors.fill: parent; + MouseArea { anchors.fill: parent; - MouseArea { - anchors.fill: parent; - acceptedButtons: Qt.LeftButton; - hoverEnabled: true; - onClicked: { - var newValue = !model[styleData.role]; - var datum = userData[model.userIndex]; - datum[styleData.role] = model[styleData.role] = newValue; - Users[styleData.role](model.sessionId); - // Just for now, while we cannot undo things: - userData.splice(model.userIndex, 1); - sortModel(); - } + acceptedButtons: Qt.LeftButton; + hoverEnabled: true; + onClicked: { + var newValue = !model[styleData.role]; + var datum = userData[model.userIndex]; + datum[styleData.role] = model[styleData.role] = newValue; + Users[styleData.role](model.sessionId); + // Just for now, while we cannot undo things: + userData.splice(model.userIndex, 1); + sortModel(); } } } From deb40f67df69098297832054ced6180a220cfbfe Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 19 Dec 2016 14:26:49 -0800 Subject: [PATCH 11/68] Steady progress --- interface/resources/qml/hifi/NameCard.qml | 65 ++++++++++++++++------- interface/resources/qml/hifi/Pal.qml | 10 ++-- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index ef3afa22ff..e65ffa3561 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -13,34 +13,59 @@ import Hifi 1.0 import QtQuick 2.5 import "../styles-uit" +Row { + id: thisNameCard; + // Spacing + spacing: 5; + // Margins + anchors.leftMargin: 5; -Column { // Properties property string displayName: ""; property string userName: ""; property int displayTextHeight: 18; - property int usernameTextHeight: 12 + property int usernameTextHeight: 12; - RalewaySemiBold { - // Properties - text: parent.displayName; - elide: Text.ElideRight; + Column { + id: avatarImage; // Size - width: parent.width; - // Text Size - size: parent.displayTextHeight; - // Text Positioning - verticalAlignment: Text.AlignVCenter; + width: parent.height - 2; + height: width; + Rectangle { + anchors.fill: parent; + radius: parent.width*0.5; + color: "#AAA5AD"; + } } - RalewayLight { - // Properties - text: parent.userName; - elide: Text.ElideRight; - visible: parent.displayName; + Column { + id: textContainer; // Size - size: parent.usernameTextHeight; - width: parent.width; - // Text Positioning - verticalAlignment: Text.AlignVCenter; + width: parent.width - avatarImage.width; + + RalewaySemiBold { + id: displayNameText; + // Properties + text: thisNameCard.displayName; + elide: Text.ElideRight; + // Size + width: parent.width; + // Text Size + size: thisNameCard.displayTextHeight; + // Text Positioning + verticalAlignment: Text.AlignVCenter; + } + RalewayLight { + id: userNameText; + // Properties + text: thisNameCard.userName; + elide: Text.ElideRight; + visible: thisNameCard.displayName; + // Size + width: parent.width; + // Text Size + size: thisNameCard.usernameTextHeight; + // Text Positioning + verticalAlignment: Text.AlignVCenter; + } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index b0710a1dda..0f52c3686a 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -58,11 +58,10 @@ Rectangle { userName: myData.userName; // Size width: nameCardWidth - anchors.leftMargin; + height: myCardHeight; // Positioning anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter; - // Margins - anchors.leftMargin: 10; } } // This TableView refers to the table (below the current user's NameCard) @@ -125,6 +124,7 @@ Rectangle { itemDelegate: Item { id: itemCell; property bool isCheckBox: typeof(styleData.value) === 'boolean'; + property bool isSeparator: styleData.value === ''; // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { @@ -132,19 +132,17 @@ Rectangle { // Properties displayName: styleData.value; userName: model.userName; - visible: !isCheckBox; + visible: !isCheckBox && !isSeparator; // Size width: nameCardWidth - anchors.leftMargin; // Positioning anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter; - // Margins - anchors.leftMargin: 10; } // This Rectangle refers to the cells that contain the action buttons Rectangle { radius: itemCell.height / 4; - visible: isCheckBox; + visible: isCheckBox && !isSeparator; color: styleData.value ? "green" : "red"; anchors.fill: parent; MouseArea { From e7c2229b2c52b2d859d09be4cd7465524c9dcf61 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 19 Dec 2016 15:31:43 -0800 Subject: [PATCH 12/68] More progress --- interface/resources/qml/hifi/NameCard.qml | 89 +++++++++++++++++++++-- interface/resources/qml/hifi/Pal.qml | 31 ++++---- scripts/system/pal.js | 2 +- 3 files changed, 99 insertions(+), 23 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index e65ffa3561..0856a5f183 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -9,16 +9,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import Hifi 1.0 +import Hifi 1.0 as Hifi import QtQuick 2.5 import "../styles-uit" Row { id: thisNameCard; // Spacing - spacing: 5; - // Margins - anchors.leftMargin: 5; + spacing: 10; + // Anchors + //anchors.topMargin: 10; + anchors.leftMargin: 10; + //anchors.verticalCenter: parent.verticalCenter // Properties property string displayName: ""; @@ -29,19 +31,24 @@ Row { Column { id: avatarImage; // Size - width: parent.height - 2; + width: 50; height: width; Rectangle { - anchors.fill: parent; radius: parent.width*0.5; color: "#AAA5AD"; + // Anchors + width: parent.width + height: parent.height; } } Column { id: textContainer; // Size - width: parent.width - avatarImage.width; + width: parent.width - avatarImage.width - parent.anchors.leftMargin*2 - parent.spacing; + // Anchors + anchors.verticalCenter: parent.verticalCenter; + // DisplayName Text RalewaySemiBold { id: displayNameText; // Properties @@ -54,6 +61,8 @@ Row { // Text Positioning verticalAlignment: Text.AlignVCenter; } + + // UserName Text RalewayLight { id: userNameText; // Properties @@ -67,5 +76,71 @@ Row { // Text Positioning verticalAlignment: Text.AlignVCenter; } + + // Spacer + Item { + height: 7; + width: parent.width; + } + + // VU Meter + Hifi.AvatarInputs { + id: nameCardVUMeter; + objectName: "AvatarInputs"; + width: parent.width; + height: 30; + // Avatar Audio VU Meter + Item { + id: controls; + width: nameCardVUMeter.width; + + Rectangle { + anchors.fill: parent; + color: nameCardVUMeter.audioClipping ? "red" : "#696969"; + + Item { + id: audioMeter + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: nameCardVUMeter.iconPadding + anchors.right: parent.right + anchors.rightMargin: nameCardVUMeter.iconPadding + height: 8 + Rectangle { + id: blueRect + color: "blue" + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + width: parent.width / 4 + } + Rectangle { + id: greenRect + color: "green" + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: blueRect.right + anchors.right: redRect.left + } + Rectangle { + id: redRect + color: "red" + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + width: parent.width / 5 + } + Rectangle { + z: 100 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + width: (1.0 - nameCardVUMeter.audioLevel) * parent.width + color: "#dddddd"; + } + } + } + } + } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 0f52c3686a..502ff16971 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -36,10 +36,10 @@ Rectangle { height: parent.height; // Properties property int myCardHeight: 75; - property int rowHeight: 50; - property int nameCardWidth: width*3/5; + property int rowHeight: 65; property int separatorColWidth: 30; - property int actionWidth: (width - nameCardWidth - separatorColWidth) / (table.columnCount - 2); // "-2" for Name and Separator cols + property int actionButtonWidth: 50; + property int nameCardWidth: width - separatorColWidth- actionButtonWidth*(table.columnCount - 2); // "-2" for Name and Separator cols; // This contains the current user's NameCard and will contain other information in the future Item { @@ -47,9 +47,11 @@ Rectangle { // Size width: pal.width; height: myCardHeight; - // Positioning + // Anchors anchors.top: pal.top; anchors.left: pal.left; + anchors.topMargin: 10; + anchors.bottomMargin: anchors.topMargin; // This NameCard refers to the current user's NameCard (the one above the table) NameCard { id: myCard; @@ -57,11 +59,10 @@ Rectangle { displayName: myData.displayName; userName: myData.userName; // Size - width: nameCardWidth - anchors.leftMargin; - height: myCardHeight; - // Positioning + width: nameCardWidth; + height: parent.height; + // Anchors anchors.left: parent.left; - anchors.verticalCenter: parent.verticalCenter; } } // This TableView refers to the table (below the current user's NameCard) @@ -70,7 +71,7 @@ Rectangle { // Size height: pal.height - myInfo.height; width: pal.width; - // Positioning + // Anchors anchors.top: myInfo.bottom; // Properties frameVisible: false; @@ -86,7 +87,7 @@ Rectangle { TableViewColumn { role: "ignore"; title: "Ignore" - width: actionWidth + width: actionButtonWidth } TableViewColumn { title: ""; @@ -96,13 +97,13 @@ Rectangle { visible: iAmAdmin; role: "mute"; title: "Mute"; - width: actionWidth + width: actionButtonWidth } TableViewColumn { visible: iAmAdmin; role: "kick"; title: "Ban" - width: actionWidth + width: actionButtonWidth } model: userModel; @@ -134,10 +135,10 @@ Rectangle { userName: model.userName; visible: !isCheckBox && !isSeparator; // Size - width: nameCardWidth - anchors.leftMargin; - // Positioning + width: nameCardWidth; + height: parent.height; + // Anchors anchors.left: parent.left; - anchors.verticalCenter: parent.verticalCenter; } // This Rectangle refers to the cells that contain the action buttons Rectangle { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index c426f3fd87..b66d9afa67 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -142,7 +142,7 @@ function usernameFromIDReply(id, username, machineFingerprint) { // If the ID we've received is our ID... if (AvatarList.getAvatar('').sessionUUID === id) { // Set the data to contain specific strings. - data = ['', username + ' (hidden)'] + data = ['', username] } else { // Set the data to contain the ID and the username+ID concat string. data = [id, username + '/' + machineFingerprint]; From feafc1683333f279ccc7b77920db32c6eae85f73 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 19 Dec 2016 15:41:28 -0800 Subject: [PATCH 13/68] Frequent checkpoints are good. --- interface/resources/qml/hifi/NameCard.qml | 4 ++-- interface/resources/qml/hifi/Pal.qml | 4 ++++ scripts/system/pal.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 0856a5f183..a600d1b59f 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -18,9 +18,9 @@ Row { // Spacing spacing: 10; // Anchors - //anchors.topMargin: 10; + anchors.topMargin: 10; anchors.leftMargin: 10; - //anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenter: parent.verticalCenter; // Properties property string displayName: ""; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 502ff16971..1c7fa067f6 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -109,6 +109,7 @@ Rectangle { // This Rectangle refers to each Row in the table. rowDelegate: Rectangle { // The only way I know to specify a row height. + // Size height: rowHeight; // The rest of this is cargo-culted to restore the default styling SystemPalette { @@ -126,6 +127,9 @@ Rectangle { id: itemCell; property bool isCheckBox: typeof(styleData.value) === 'boolean'; property bool isSeparator: styleData.value === ''; + // Anchors + anchors.topMargin: 10; + anchors.bottomMargin: anchors.topMargin; // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index b66d9afa67..e5d28e4174 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -145,7 +145,7 @@ function usernameFromIDReply(id, username, machineFingerprint) { data = ['', username] } else { // Set the data to contain the ID and the username+ID concat string. - data = [id, username + '/' + machineFingerprint]; + data = [id, username || machineFingerprint]; } print('Username Data:', JSON.stringify(data)); // Ship the data off to QML From 7999e14ecfe0d3b3a480cdd612659a7423bccf1d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 19 Dec 2016 17:12:53 -0800 Subject: [PATCH 14/68] Kinda screwed things up by changing styles --- interface/resources/qml/hifi/NameCard.qml | 28 +++++++------ interface/resources/qml/hifi/Pal.qml | 49 ++++++++++++++--------- scripts/system/pal.js | 5 ++- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index a600d1b59f..3183f8b6ce 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -18,11 +18,16 @@ Row { // Spacing spacing: 10; // Anchors - anchors.topMargin: 10; - anchors.leftMargin: 10; - anchors.verticalCenter: parent.verticalCenter; + anchors.top: parent.top; + anchors { + topMargin: (parent.height - contentHeight)/2; + bottomMargin: (parent.height - contentHeight)/2; + leftMargin: 10; + rightMargin: 10; + } // Properties + property int contentHeight: 50; property string displayName: ""; property string userName: ""; property int displayTextHeight: 18; @@ -31,8 +36,8 @@ Row { Column { id: avatarImage; // Size - width: 50; - height: width; + height: contentHeight; + width: height; Rectangle { radius: parent.width*0.5; color: "#AAA5AD"; @@ -44,12 +49,11 @@ Row { Column { id: textContainer; // Size - width: parent.width - avatarImage.width - parent.anchors.leftMargin*2 - parent.spacing; - // Anchors - anchors.verticalCenter: parent.verticalCenter; + width: parent.width - avatarImage.width - parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing; + height: contentHeight; // DisplayName Text - RalewaySemiBold { + FiraSansSemiBold { id: displayNameText; // Properties text: thisNameCard.displayName; @@ -63,7 +67,7 @@ Row { } // UserName Text - RalewayLight { + FiraSansSemiBold { id: userNameText; // Properties text: thisNameCard.userName; @@ -84,11 +88,11 @@ Row { } // VU Meter - Hifi.AvatarInputs { + Rectangle { id: nameCardVUMeter; objectName: "AvatarInputs"; width: parent.width; - height: 30; + height: 4; // Avatar Audio VU Meter Item { id: controls; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 1c7fa067f6..a977a77f64 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -28,6 +28,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import "../styles-uit" +import "../controls-uit" as HifiControls Rectangle { id: pal; @@ -35,10 +37,10 @@ Rectangle { width: parent.width; height: parent.height; // Properties - property int myCardHeight: 75; - property int rowHeight: 65; - property int separatorColWidth: 30; - property int actionButtonWidth: 50; + property int myCardHeight: 70; + property int rowHeight: 70; + property int separatorColWidth: 10; + property int actionButtonWidth: 55; property int nameCardWidth: width - separatorColWidth- actionButtonWidth*(table.columnCount - 2); // "-2" for Name and Separator cols; // This contains the current user's NameCard and will contain other information in the future @@ -50,8 +52,6 @@ Rectangle { // Anchors anchors.top: pal.top; anchors.left: pal.left; - anchors.topMargin: 10; - anchors.bottomMargin: anchors.topMargin; // This NameCard refers to the current user's NameCard (the one above the table) NameCard { id: myCard; @@ -66,7 +66,7 @@ Rectangle { } } // This TableView refers to the table (below the current user's NameCard) - TableView { + HifiControls.Table { id: table; // Size height: pal.height - myInfo.height; @@ -81,29 +81,41 @@ Rectangle { TableViewColumn { role: "displayName"; - title: "Name"; - width: nameCardWidth + title: "NAMES"; + width: nameCardWidth; + movable: false; + } + TableViewColumn { + role: "personalMute"; + title: "MUTE" + width: actionButtonWidth; + movable: false; } TableViewColumn { role: "ignore"; - title: "Ignore" - width: actionButtonWidth + title: "IGNORE"; + width: actionButtonWidth; + movable: false; } TableViewColumn { title: ""; - width: separatorColWidth + width: separatorColWidth; + resizable: false; + movable: false; } TableViewColumn { visible: iAmAdmin; role: "mute"; - title: "Mute"; - width: actionButtonWidth + title: "SILENCE"; + width: actionButtonWidth; + movable: false; } TableViewColumn { visible: iAmAdmin; role: "kick"; - title: "Ban" - width: actionButtonWidth + title: "BAN" + width: actionButtonWidth; + movable: false; } model: userModel; @@ -127,9 +139,6 @@ Rectangle { id: itemCell; property bool isCheckBox: typeof(styleData.value) === 'boolean'; property bool isSeparator: styleData.value === ''; - // Anchors - anchors.topMargin: 10; - anchors.bottomMargin: anchors.topMargin; // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { @@ -248,7 +257,7 @@ Rectangle { datum[property] = false; } } - ['ignore', 'spacer', 'mute', 'kick'].forEach(init); + ['personalMute', 'ignore', 'spacer', 'mute', 'kick'].forEach(init); datum.userIndex = userIndex++; userModel.append(datum); }); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index e5d28e4174..916556fdd7 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -79,7 +79,7 @@ ExtendedOverlay.applyPickRay = function (pickRay, cb) { // cb(overlay) on the on var pal = new OverlayWindow({ title: 'People Action List', source: 'hifi/Pal.qml', - width: 480, + width: 580, height: 640, visible: false }); @@ -144,7 +144,8 @@ function usernameFromIDReply(id, username, machineFingerprint) { // Set the data to contain specific strings. data = ['', username] } else { - // Set the data to contain the ID and the username+ID concat string. + // Set the data to contain the ID and the username (if we have one) + // or fingerprint (if we don't have a username) string. data = [id, username || machineFingerprint]; } print('Username Data:', JSON.stringify(data)); From db2da8f06b8c4a581c00a2fa01df954f9838fd34 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 20 Dec 2016 11:37:10 -0800 Subject: [PATCH 15/68] Great cleanup --- .../resources/qml/controls-uit/Table.qml | 4 +++ interface/resources/qml/hifi/Pal.qml | 33 +++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index 35029ad8bf..865e24945e 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -20,6 +20,7 @@ TableView { property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property bool expandSelectedRow: false + property bool centerHeaderText: false model: ListModel { } @@ -34,9 +35,12 @@ TableView { size: hifi.fontSizes.tableHeading font.capitalization: Font.AllUppercase color: hifi.colors.baseGrayHighlight + horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft) anchors { left: parent.left leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding verticalCenter: parent.verticalCenter } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index a977a77f64..7a48619550 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -31,7 +31,7 @@ import QtQuick.Controls 1.4 import "../styles-uit" import "../controls-uit" as HifiControls -Rectangle { +Item { id: pal; // Size width: parent.width; @@ -40,18 +40,19 @@ Rectangle { property int myCardHeight: 70; property int rowHeight: 70; property int separatorColWidth: 10; - property int actionButtonWidth: 55; - property int nameCardWidth: width - separatorColWidth- actionButtonWidth*(table.columnCount - 2); // "-2" for Name and Separator cols; + property int actionButtonWidth: 70; + property int nameCardWidth: width - (iAmAdmin ? separatorColWidth : 0) - actionButtonWidth*(iAmAdmin ? 4 : 2); // This contains the current user's NameCard and will contain other information in the future - Item { + Rectangle { id: myInfo; // Size width: pal.width; height: myCardHeight; // Anchors anchors.top: pal.top; - anchors.left: pal.left; + // Properties + radius: hifi.dimensions.borderRadius; // This NameCard refers to the current user's NameCard (the one above the table) NameCard { id: myCard; @@ -65,17 +66,34 @@ Rectangle { anchors.left: parent.left; } } + // Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle + Rectangle { + color: "#FFFFFF"; + width: pal.width; + height: 10; + anchors.top: myInfo.bottom; + anchors.left: parent.left; + } + Rectangle { + color: "#FFFFFF"; + width: pal.width; + height: 10; + anchors.bottom: table.top; + anchors.left: parent.left; + } // This TableView refers to the table (below the current user's NameCard) HifiControls.Table { id: table; // Size height: pal.height - myInfo.height; - width: pal.width; + width: pal.width - 4; // Anchors + anchors.left: parent.left; anchors.top: myInfo.bottom; // Properties - frameVisible: false; + centerHeaderText: true; sortIndicatorVisible: true; + headerVisible: true; onSortIndicatorColumnChanged: sortModel(); onSortIndicatorOrderChanged: sortModel(); @@ -98,6 +116,7 @@ Rectangle { movable: false; } TableViewColumn { + visible: iAmAdmin; title: ""; width: separatorColWidth; resizable: false; From 5b77202c52ffd24e251a214ea04c8422bfe588f3 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 20 Dec 2016 13:23:49 -0800 Subject: [PATCH 16/68] Popups wow! --- .../resources/icons/defaultNameCardUser.png | Bin 0 -> 50400 bytes interface/resources/qml/hifi/NameCard.qml | 8 +-- interface/resources/qml/hifi/Pal.qml | 55 +++++++++++++++++- 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 interface/resources/icons/defaultNameCardUser.png diff --git a/interface/resources/icons/defaultNameCardUser.png b/interface/resources/icons/defaultNameCardUser.png new file mode 100644 index 0000000000000000000000000000000000000000..807ca2fa87840b94778e027321d68cfd0288108f GIT binary patch literal 50400 zcmV(yK{NQj(2BluPw`xhw|u!lK2XOKm+~WRdw$oKE{18wCE*qc5>W@- z5pmoN*Mx}EQBetu#8|5XmzPFdS{iY2T(i_PyxKrr*Hl%79A6W%3QXL+6_FN*!|PeE zG`!}+>j2n2{f4c>Is1!_2lJkfW*v79=iFN)cDlrV-(#L|6QV?vNV1HG;6ZRQ_#8AK zD4GLgLsdbA-i?Zc3z&lmIEuI;63RWr&Nt~ZTJiiAJe$?Il2uSu6vML-&8b}o4)YLj z&zN_J3L%sOASU1yk%WnnG$Wp96)ra;t}jiwv^3$?v}UOtvDDTyK~Yag?ua;1Lv!G| z9zs|bktyy|cb@_ipdDWEa{!IUf#U$!0S+#%gq;iCZoRu5otvvntK$8 zA`(A`?>jvq>Np0FR7*d1{J`B0a1PvLOGI`=sx4qgB0Ec=+ERQ+M7EN7KI{^^V>p;7 z@2_U|`p9OqV0QK&U!$pYY^F52!>uHe{USPCQRf&Oze~m!1R=3q(^? z1R>x+ikXD6Vm(dW&{Tn?C~S-xUS1w^W3t40J7OFo1aFe04X_PWUl&-H#8k{Cv&ttT zOoGTngACC`Y6H?Dv{~YDMA52hboW?z6QX>6-*x&V0ND>mDG=tsTwLbrdLXVRX-v~K z2TAgQ1=}(80}FX;sg<2cGu~opY3o)cA04E`gV}<+yGPvJTX27o_;9h{;e0{P72*m} z$SN!>V>W}Jhzevq-=ycD@ddk)sX8I%+#NRqF`{Ies9YThH`k|peSMAR+J@!e5S&He zN*D5GZ@ww+>zZsG^QnkTkuXWh#GIR=e^rHz<> zUbiAW>DS|aIf+Pe_he!Tk-1?Bn42(PjBGB(oXk9Q%vHjHs~qH@JFdFZ^z*G%mAlL1 z@!jV~leg_!b$A4Puz$!0+YkANt<1y2eRjJU3o}FlwFc@G&}7Wdvy*2RRi9T+p3SOU zNa8HKC#Z92RC9BAnOmzD*j!oRVx?5I5@G@t*G0G?BAb|QxXHSUtxJfL!b@liOfXu< zO7wt4GmEH15s!vP5sBFyGmCY>{}Z5a88RdhM251$=ey2;hZjIgj^UnD5KV|Bv7AIC z_2LOMiG+mfT+xL4T+9!Ec}Lz+)g2YzQuDhZ`THS+w|nQUS?Y7vz+TGSnJxIm_8vdG zx5M4To=$39Dr)BEoAf-=_+qVDSa6{oaci`~^OG^pOc&-gqm2U%^a7u!!29h&!}_iv3s8IIm1XYCJKj6oPGoL`Jrhx-F=0bLWEnv%qyO_VP*!wQ>eh2UD&KKM}IAr^1#?KFHw)PKsFrTyM zLT84$6LGK(WrQhYcMvDKAQ(m_s}a09KP2ao#@TgLCxS#IIg*4BA~6a^rpp~wtjMWH zH8KtrjbtXlxjs^Etk-;N<9V7o(g;l>b|Z@4Ftg2sYznfjXMtk!2|)2Aka#xVQK`g` zDd3Pr&(ss@6h8(OXFw$60s<%RIa`e|4^IS$)6vgo+=KBHV8ka7s}mlTa@TLQ6c3F4ARDw>#Y{Dmtt>SMQ$i>T{KR)Ub{(04eUh+vqQSe7kA__&Y zQ|Zrfv~fCW`3w=qXN^cs|C}HH%=m$d%*E|Mu^o5W!Syc4T|?i_;qa$I{8{pvj}Cj@ zy!()!+i`SJ!#@;)H302B(HsgL5m#d_zPw{UrU+sklx&%cSTgN;&lN1d6kd{6P|VDnd9u z)b57rC#NRI(4hV}5Xs5kN#WyvSAKKn>|-Tem*J=9xr)z$gXm#LogL7-xw7|j&TnVq zr^qLkFbyaW41#1-m1O57+w=6~@m<*HKpHBwG-%EQ zdX~ox7pBX6W2NEJc+Ayt#d@?*tK4??TQ1cN(Xi>>ZDePCP-9MlNGoV7;%z8p(of?l zexqxlJtBf4rw+d(UJkhVlTwSbM5c@2@`x94`~);~0GLNW#toEB6*h8C z>$S%zFfqI}Av)e7MAXT66xZMC{jR+~QL5?*QTgal=WsTaICJ;`LO3BkeR$BjJEk`L zo6VW%IQAsc>tx9shK_8o#;f zhl?wt6*em2SGx=D%re_@x6z>ZxLJv9M)M6Mt!I2{E|VDR){@1pO07CkTnP@? zaleP*0?Kc<&2le0i#~DGEja2k^OTvJG0#0+GI}CrhMAp22_kX=1VJ)Eg$kvOfhGn< zRb*5}Vo)weC4^AAcOOete#87mxyzc0jNBZNKtRYPeM*L-mXm~#T$r2L95_(b9dWkY zeJe70d+xKhL(Fdml^^A#`CcFRyZbx*)!Xl|Gd~B7uN-Jp1A&#?N{1;Bc(i%N7f7tg zF3>y8;Ioh<)2xj6*Q1_qzVrfDW6hFgswTdiEWZ|{z7sl^rM{ozFF~*aM2Ua0EE8pxz~3b&KK;=d-l4_euBMZ z94rbz9HkOT*l<0k!QmJ*pB|FRBLH~pRsP(F0;4Lh+Egq>VJQeJqlU?-<(g#1<1tNB zorupk&56*R`P`>d8WAB|pa@$#f|ke-6qiWKf@t>`ALA!LJzV1_b>bcXJBIHla@XD8 zcK5eb_05d`c)y!-I6LA851gOg|A_aF=5$hn36%>*j|8wjpfS)}Ml4flK*Y&zSa8(7 zIzjzc=9Bay;CAv~1HHLk7KTw9(pv3xONuc3C^ z@SB2fW_&}@Px7aLL9YGyru9=CLB#Q_C*i(H5lDG(I!S~)1~TdbV%T6pQ-HxyoX(ts zu48ABc-STOdgCA&^S)=l>*)H-EG6dInVT_7&O9ZOIavM}UvV1X2F~RvPjY*@6kP*g zR2mq?iZ(#4LQ~Ias~O`UG~tk?pezSitpaOp#Y$5#t}8~NF1;+D%Kpd#tN-2~Y z_n`!AM$jIuK(d_WR2WQ*h6<0WK2Dy-Q{UypJx^WZRFHKs-*op)4SI9Zj^1qR`e)w^ zb1sfH`1O9rFL#c(yI*=-G?eR&(aGMSDukeT%7sV{g1LUBXw^mBFged zk1Ujc)`6KPLdslUZFqHc%r{q;cx7dR)op0q-l($OM*L=<{06EUF070AX^q7bMOcsL z?=Fv(EQfsAFREA}rjQ^)OGFJ#!A{iM{!PhqI3RhjVu3JqLYe z)@QPZM_@2iIoy3vw8o-C;1Nync(Fw7>Eb05;LFh#n=1 zF&0>9BNy75wNb@%)G}!+E;q$oXcPit6-Jd(R|=lVxrb1=6FeZUs8l#Tk`3_;GMMm~ zoxShspH_J(;Gxor_(agDs7~E|-Lh?}%8gpW%@>!eH?OqSJJ%+ewV?dOdhRFV$e`;p zL976sJK2hAC5Qq6mGU{xLE{UyOpp63j#+!f!w|x{qsfq9G`8URal>~vE_3_J1(vi& zC4VE(-cC z)v;1nfmlOlnTQY_JolySE1>|Q2<}!eO&~K^1p#Y-K$H&58k`aul73jhZHH)kwocmj+FAfnc>1$YJFumjjV>6 z6C`4}*e7_<;w5QLkdBvDmU;c^C2n7ua-oJOem#i12HfsF-B6KDA*>s+?n7kZmSQCu z4gE}3dB+>%^x<@OLI}9`;0eTrgrg!`#60rAcX@w%&d;_E`Q`Sa*xg|S?|=?ur4W@T zjG_%-PLYzH`q-1{s6*ybQ}e>=l#P`s-&~upT2-t@Db|QMy*SweAVhl<>6M>$=JS0T z5$$4W;F%II4}{}Bmje^FEQh-i^V{zIPX+stX#L||t9bMN4sU+&AwRi4XF(<@1g#Lt zzI&7|ou?;XJoLj5FB{S@%t3RaPMOuJ;#)6n^1{-Xm&Xm4Vs3)^8@+Qor*JD+*tE(w z5LtJ(iDlXlLaQRtk-}H_)9=;eU~zT>gC!$+APBey_H*XW;f!~74!L(QFh8Eo zIA;!}TXk|+1s&3^08-KgOMXI}x)|n0P(2}rJ1GgJB2=_U@9Zw%m$RO`+lTzqyIX85 zjd)?XVPmD?!g9m7j@05b5=m~Sn&`Y_g9FF)Su`Rd@`#r*pJdO;T_Vs5GN~ie4yFqU z>y=a+b!KxVcR%?3mH5&1(Pb{IDZlvO1Mbge^ug&;;k05XM78KS5&DH&daAS1C}N3a zajuRlesBE}zrQ--(%LdD{%XqUHAQa=a>IlTkG>wYngWyLxm6*mTf{-6IADC*wmV&k zI1L!bzi%P3z3AAT^=!=!xj#>QI9u@H;Q_nzj-4)@CGsSfgN-0!^jP5yaC9IpML2m< z`#@3(g1Qg22RWfrj(yV41SF>(f96kr7C!!``S@Cg>#fXiD-CVsGZbQ!fnbfUC1q0| zq8b}Gmr~}f6)-D?!x|t2dJ(q!1>60CzJ@zp&%1{e7xx-2EjL^mMXpwXt4kA_HVmLt z_JRzbTb$1%lCd-p&ngiiISd9z8gbf`7p(+sgUHmKX@d0_bYrb*el)J+@2k3E%~$xR zE#t$PvEzohknC)u-#KV}!ImF8H=!g?FIwKn0~FK@Eu`;F3nPja|TsBd-dn;F}v zDw#r_Xo_vdT6JOw0dbFJh6g2^5k)`C)9P$=VODsEoOx)6+#Q(rw{~ZIbks2$+VKN| zOI6aClaM70hq(Yx1t(AC?1o7y#x^I7cJWaZVx_WJp0Y!#VD%7DJUQLS`g9M?fPGwR zaT;i}2lo;oX&D`mPk>aRGRz8;1q7A4l$=e#pg$&(wUnaGa!JKPi54mni(a_XJ9lP@ zuzkR^a(-`R#MiC_URY$Rs-mfDVhlvlLO^~-^C2CGVmo#{)1jp79uZCE64i-~rB+qj zEHdqpb(OH8GTL~qI{KT*3zr@=%S-s(ZESDB{=6rpM64qI6|yM((r_ACI)t{&2f-2P z3r8Ro*_DwiNtL;Ej6%nEuTA;xr8Qn$Tf>DH6!{Kt8@S0MUyI|aNHnuJl;z?hUB#pD zyRyif3{60CCiFeE1bT%IqxJ?Q7rlWv<`sul@&5jTcOUHXpSO?L+uNhhsrYi3oov?R zcA_acU;kt4Mm^%Wixa+nWrY`~4Xd#LWFp`M%u>;~kT6RGDZm!IdQR0q%5~eggJ()aP0RdT{7BLe(I$ZGP)jvC&ylz-ra|GJ1{ezv{GU%&eSKY#dunMFL6 z?>SVZ%3@~daRet{p1~|%n)D=fLOqJi(u@!SRaN1AkvT6GN35*W+*lg($CoZ}d*rn0 ztDW0-G}dqQv+ib$;Ra_FoyX#yllyR*mw22POjRlOS)Yie;8HXY0h0CXCF9QSHgE6G zcz@>HIh^xgmYG`sjl?dU0LJ6hm2<%OmoGz^OT~fI?4`)=_JVf~?(y7`a&@KU)?^Ih z5!OV!Ul2NF93xo+bIYYXt&WQzTEILI@N8IiLO63?PTL!_KpUvqEVL4RT6cMUGC%q+ z-&|@Q#OKyoUCR8^ePQ?TfZj6*ShiBxld>oBm#DrH#{d3*{Zd@$LclCBt}D_!<6aRo zV%;Iv)+f9=YWVi$b#7l;p$hp`ZL05k=ijbezAogKP~8{+V+E*(*BQV#T?u(yIXUVg z*@aXoQY)4-mYvR>ySsb*_(9?)50CiC_CCMfpRqU3NERwUQ$h!EQmJlys+-mM`sY6V zG6Z*m2#zu9dLA6kxj%RA_caTHS%zAiHaH<#sVJFJSw#rPoRJTeB`sPPlqh=oA)-90 zPwTLMYBgHRc^m`dpgKv`PeYV6sl)vXK?z=yykocTm^)Nb(^7#JGint*2aRVQebxa| zDzi}I2n!8rJy%=d)`c>NOz1?T04D5zwVRD^S(qR+yE{RO|++uVg#yxprO=K1tVee?R>oPpXYXjvJPSx9nUgG|l@4-& zbI^FU;b%8vRF7D6hEx@)JLGbm`S-v73g5YUh0Efr)cR*B==Y+<+tK4o9@z9~>nZmW zkvJZr3w=^rX81|@@WjDe^`y(wNC=P}c9Zavtv&wYXTRqEedid?|`$E0Xn4p)q0@i4uVb4j#x<>`LJ6 zqeFhRb)Un-IU}tJO^ZcAMRB+Ceo;ijSvmTXfkR^*ad#C$6!+RvYC{NZj5i zT$Rd6YZc=Z37vunxdsoJ>~nk=(|B5)coM}t-eh&Y{`s7j7d=Ffp|WP67E!AZZ&68@ z3c0iil9SVQnZ0b>S!BX|2N$JnE2-c|Cgd3eGiExgahRk*dwa+C@=3b|iudQG{e2y=I8z|IzH^%CRg2 zkCX`)J^MNF>-~;5w-@}^`yIdDp7HQtfr;VjWY4Gs)M{KIi<7iWDMudpWhm9)b|oK6 zQO@~{XS9(A;?j}Q(JMoEP!wbwD`ZRGX>lFPqU`s^-GezFCLwubOrTWBOa7K!tb3)8zRtE-E}{yy8eC#QrH@z5~0 zoh&&gg>mzSc( z4L_^D_*h4(h~XjNDv0Y~T_L3tR!UDql_7V=2VLeb-u;mO{TILH&dkw1(pe9oxHA=R ziow}?VBa~97QXQ7X(JkXEk3Hd3+E?W4|#9@kdfA`)HPKdF;5UE1GP+$BxtOlj?BEL z`gkO8y1GI@qNBCD*Ak_5$rHD3yj)dVFHYAEsf2#ku{Se1Q3y&5pjJjja1I(DUzG+H z^vtcNk_Jmo6$4k7D*n}_RsLl25?978mKXoV@$ae0H)EEUVpUx`vc-icVf;ye0dS~> zdO>XjEC@MJx-e5>DJ$FUQ6+r{u@ypz);w zjK_habubI`L^^~$oOk!P+1}q{I+@Zm9pXyNO0*i0FbvpAsHIP8y+5w5#AGSfp^7=@ z+TH3{$J%lpYu8OyLfpB~HoH|2jx4h`UqBMn0y$+u6~9n4=@$kx#I3Yx#|9x1-Fa@M z;kC<4{Hx2$JXbF*b>u(qH2+aWyj@#($&t%hd+5zn}^cbI9gH`R!C{@@V&+pUf}eI;m#4Au3c}o(d+$ zLF(C_E!Z(3EatRv%s52cI;_eNK$FpCC4X|+Tmki9;Akj?{Fq%@4Y3wN+bz;~6zpg+ zY92IA;Ap;JzcW&(a1rziwza-E=?R%iE230b?zlRR{OilBynbngE0cIdz5a8n^xFcr zN%;m6F86_zlv6v3p&f?(d}4*9>Ib!;WynUQ6)qJI7nxse@9=jIdVcioJ?{60HW9TM zv7ZBiNsS6accu)@PV!mil%B6I321l_NEr`K3JL9bDDD6NAOJ~3K~#66NrBu8AI=u^ z($JZ(+(ecu$MdX^qSc6;m4Rkc!AZ4Rj&sm!NMeLu#E z?S8?7z9$Pscy^wiJOPe_6jQ6d=R(YU_l0%dxU$C8$@m44_-9G<4NKjvI`F(kz1*3t zsD^P}RdwzYpXPE>Mtv0jMq;3kEgyEqKR(#w|9k&FKmXt%xvn|#gn363BV-V%!2)in zfP|k&I}7)DuCw?`fJW6rUk7pwgd})!tcpko$UJeUOWfJnrgLX?b(P6zOhCyF^N^`N zg$cKu2_dM7W0t8j#u!6%%XPBcqS{vG?Q+$0tE1*4_nw3KK3$*4c20yo`OC{-F|e_= z!XI3_$iI1cg9{BWr=0&SHh$fue`QqZ^Eq`F=A=m#t2Xu?-Ha4N{4}7SSH-P7;Pe`J zb8CzL_|u>BgAca2vkSH})eC&FYI>iq-Z`IztHIbeJ_ z0OCpWT8>w=^8H#Y`vuR#_pc~{E;yJFshMTxep;|^JxvH)Y{t0sWh8|t?n)FvvSB_i z;|&O?1VV9BR}mfbBq4&<5wCO1(Q6xbmLf*CJf3_sjsczT^7f)7cu(}6;GwkDS4JXZ z#Lsm^!siAWDitIUg;+}o_c2TY5(6)^f!kLv^7YGWTwZFfN9_B!zmCW&?zSl+7bqpw z(;AGE)#sk7u2CX67mt3CjCx0-M)oo9?augz`w#f>_BQYD&sZcQ5(y@c`|_Tz^YwUw zG1v$yj??u$i}{?~8)(hixFVPgx*tchxTc9l<__WG$NEf37Bpt0HmC(@qiYNy9EG6w z+a|Jev|wgEi%^ks9>!BEE&(N<*gk$9>vQwdh<(H)(bFNFfeWFkFf&HcdHv#uTNfu> z9ak4?OWzmqH&DGL;+qh{SRjNws1&WdFzj!Y4R4$SC#RKDL3Mnl^`raW??g;1uCtPP@N2` zdcyfjKx+byOv|t+k~nGPo;KQQzFbd#Ia_4@uebL(l1Qv0nvLEBtpfDj$%u-tRHR|a z2(=bVJdJp$L4~Ef;OiHb`FA(3u`wAnBka%J(idpM&vrS?cAN>=Ff+-~oDeM)*mF>Q9FxSofKtdV(! zhesXn@7(9g+7*^VpzmhHQAJf%q+|q(h(Vm+^Ve>&Ryk4p&#?4IqV?B} z@VvXOtLP-C#xf1)X^~NJwKg||9#Q4c1OIq$pTGF=FLJ@nq$J?c3~i7*mUKPOOsu5ApT8)$gQm>HFhP|7^dn`=vad*cGPR$4Oh z$GHEI3tuojwQVLD^ay(JvCKq2a=Kf{{tlhK}f`g)&LY z$R3Ygj6D<)MI^dNZGC1^>$~F+deNGPM>A%&C{1`$f3E5E=LZ^M1g{VmYFqH?#bw^O za)DRZR|uZ}AZYbHL|zk-=fN+l=t>CMA|6e0{Im&KS_Lc{*X|;VfCL@HeRvs7X>K%Hu}D4pDRF?2&6(CCs^F#R z>{B+FpkShc3`6uhx_DGH;;yl3V@eBMs`OV&G5EraJ3IRj8-g33=VX=7Pp4slV>gKl zqri7x+TiQUV^-_x)me}~sj|N2*bT_nz^4kWTaKAN5*+;$JB%boFehc<=lAybtM@2bVkGqmZQL*!ralW1sX%rg-6Usm*lj2SvDv}$uICU1#$o+1? zy@Oq*qn3+RO)XCN6r$28O_Whog9=VADUhQ9kI6M=5pAMtE9uwE)64v7>xiQanrE2A zFCS=BIK2v4K(e97WAapr_o6%2RJ8p*|I0UTa(i4e9@SS|<@@85Ue78wMdX?gR)kQ4 z)FO%!RK3hQ6-y9$dNuGsFa^uxI?>pI2Ojy)A8qp=-us9Tj(VagYEBnRuSm%&k6#?q zT3-pkcv3^w+1?mC(V7U3TP~kpj_-q%(U!-Cd19DmvSgC>a54kL$}lASBqCOX;KQg! z&j=Y&!-kiup&q`}_xHG@dF&x7rO_F8Lq*C=SuM3Vm&l<<-rw7!x5UNC5=%`nlBx*M zC8UllLM8|_y*_h$^D7#?-Rc5-LPItyK2G18OR7F{tZT#$*7L`u_Eh`5$N zFaK67|K3-eGAhl}d59STo*^wVeb=!&o6%~`%JKxUo~j9W0yV)bI0}NxGEVw(>&YHx}I4~W5**Ahe^q)cc|;( zY37WZir*UrzO#9ei)HxC_qyD_p~_9sdc*yUAI8V8O*+i#VYHtjIb)VcF|u=*`0)ed zAMfq+-cb*sDYGQhz>H{M^1Dr1#51E4O1hRQlieLB(Gesd@pS6u!;IK`NnY__xz0nfgEb91-?+H?fgde+U`Ool_qf*OAssV49U2tv zAGJSxp&|{}(m@(U2`+Tmk)|S>(TMQ;(wINoobu{wP00C=Jyd_>!wmV1{@uprC?;@)T8!07EL03E2r5qL)EZVuqd!JN7cfS`5*zzO`BL zR5{*eLoy?Qq%c!uHzm@1&h_b(8bf0lmr*iD`J}lgCvN8=j=4m42tifMh`82Oc&ky% zNABEvxI?ESN>8TJS%>P7#(xn(0}XiY2?&PbRU`+2{)ns7CBA#AIFTZQ7EV%&EP*VFg-A9%89N!Y^iD(>BF7DEIBo8;r~A2WC5lS zvpcS$Dx!|5V~2XNlR*j&jX5Jk<-~;~`?*j4QSB z2iGt1&9ya_BiFObcRa}LzUyzapFLxg)AiXV(M#w2Oa#~z5Dz>Z-2mp zzQP042yR{kUZLcSI75zYUwLqNnw?!v0|6{X5Qw5B8Z$#oxY!C!RkPBLSZ*tpVjMuD zq$HgRsn5()X4V_~i;nHtf`i43E*mZp%U(P#MG0EEKE;ik`p;A4{M!Ky%1mBNa49LD zhGO;<98!Ya5c$h@?qL08USA(`ecGZ?=@XyUITGDHi4X%iNjWFMH-m<`dp>9aTeqew zfA6CScXt1ghZ5+zL=&oERE9m%Nbi}}X;ihe{CYsLF$VtC^~?Oh+6v2~)L4@5s5ZBw z*o~~Vp73d}+7`L&({Eb{U}QRk-5mJYgG2u2oh|O{FKCt;%$(k>jL{>2C5G&q>e&4E zcOkG7!9QL}73?e{eYGhacW2 z%0p8`CMq!rvKf#A7Y>5votu;Pm#<&Cz<=AF@nLUd(_%@f zjz7aM;MsvjP|OlCOjHjDmzTzT^Xhf3(&DHs6R*rd_f&R8`yAjGFqX z*OGLBCZo%dU+x_7w;w!UYu_1k}CZUuNzvkGH$rE zvc#oH#l<$T9GvAQu-s%?Z%f*%;bg*G7LZWF?mBm+r9788Evgk71M`%a&F5TcGqXv< z{k0YD&wK73E!diM?968@`X2dYqVFrQIug;9=oPL)FB!wh5)hOm9r19%g*f8=Y|ek% z*`o@uboByP+d%&*ejHLnWYMz|LTKHYq?Fey)f>3aljfZ!bz5(|u(|u-m+!Iff$gIO zwJ7F`XL|oXGiW?+H1n}g*ow8g&N5m@nkD$=TFWa9coSc(a`PJaO%G`U;?#{clsR4GBd}xDOTrGjviE`7;z*^qjneq2q`~33Y2n_*Jhlfn?LE{g_Fql9b%2@d;5E@4} zdU1M(D%8}uOn&pAhA=crc{UnRn$eh*RuOTs8+%>HyL$(`dl>j&7WrUjkL`Yh zr5Q+ZKr?Ct#XJ+$FsUGAssXf&Ja|Q9v5KODz|xH5%<$sE{P0F4P8!u~4V0gtjl7Qz%8NDA0zW6N7a`Ze(qC0{zyF zk^YC**IIVS?DvkR(#+=MGi6sFxGi_V^FYi#Ya;YX)5lJ~%rZfhI#yJ+;FYx{{`A$C zS&Oooa{qmm@H%i4*r4b!&Jdp&!$KK&qh=5ss|VR`?`W*}Fu{NN`P;npV27;2BFzc0 zLM8H5Uo&x3LK(Z{PA7&{f~a7Nj$A3V1)9F_OF490S&n?~`X&DS+Eu>(+%-0rS6B&w z%6uS2BQAl=vnVblq8ibSrJ!7%OnC9a1)g7CVk0QC#U5QZqtl9{5w)I1pwUR~1y{qc zavejaT7^hOB=&;qmpf7{*k_Q8$bh%A!e2XpfaL4O#qE< z+?PC=Pd@oP|NiY=zPT}?Pwii*%AX101KP^;#8IH@6BTtCC%(p^L6PiuPQ`^@b4l0~w(Uk=(@7?52!F75h5z-N zxA8X7fE{^PR>nyt`Y z)zF{>2TQ`kdB*~(z~wM`n`kR*Z-V&SVuE~kfRAPM~5 z)yw?(^(*}AD`UR7C2Uqb5vVBEepx7nQ3GqUyx{(&NaSmbh%4rTYs5kzmFJjH1uBHi z+PN{^;PPgJ_0T3L+_8?@>%l483%4K?a#_;ifG4jwlq%WjB#~`s!E%|lgwdEseT%2e zMpLspj#N1l>PTm0Aubq2tM-XYe)hj^9VV5Eh9o&Hwv}JjRpjnT%e~HVFT7Bfvepqa zpi+#_O%445%3m*>G|m&xJ&SAPAZ(cOZ{Ga|-`NzVn{r)3{0s5$JwtAZaE(*T#Haj8 z1fj|x!RfW2_Ei^o)WJ_5?ekxL^N2oHq&}5(cCG0bJyliV^50hO?01qhlro-kXmc*r zfd&+zi51_UR{Zf>yL`Ai;o62UYD!6;vk+1sR{;Z(l`gwr7lueCl*jkwD$TYd5D20L zC>5m&18$KsqRPk<(>k!*jA*26C3g-jndDl0OD!W-M3Dr?#mUhLuectgsG_m3K}%2W z?N~JDIk!Yh>S)XQx_Qx9%J|{H%^Wh^T z4~$+dE&o#Pwh;cLkovxgO5=GzXr<0KMiu}5)(viKR%pdvq~w3*O&V;sjU z6$-i=F+@VwmL!s$F>rln%+91@W{HEgk$?U6F8}V<4Q@;-HmX?2T3Lt^%ITp(3J^_Jl64|h#;SlwKte2wY%c?YRzjC^ z0o{Vxd}BK0daSv3ctkrpE@NJexDCmTC4=GBJ`^NE z?h}vu8<|=xoR?6U})FcwDfzUXxS`9lT?r5 zFDo(b&Ww4tpvADELJczPHwXDb_y4y_8fUAF5Q@>D1TA);Rhke z`>k}lmH7k-=e8whaxr~f{xNkD9vmOOeVJY#l1Jcg&y)6&ga>bk1R*^0!C`SXBsljTkpKm8*O2aeQ>lao^Kvq%Y-5 zS*cTf*2lIiYT# zYq@!Oi~qD&vAs2>jvId-I{qwZ_)hk`CxmGZy5W|iD6!0*hIPQzhkyw+3CkTPq2d4j z^?iP{pV;pbQF<82DWV0ui(a7?2@+n5gdvtmD#g>nGkp!gGa)5%Re&zyh)N=Fk0bxX zcW&^(mar9v*}g0*iD4~$P>d@EE@kgz&?#fNMN$p|UOHivzo%?7FU_!5w03{|dk6(E zRbM79K{RqH9?*oyY@rSf6Z(Q&o2iGrbHM#b-YMVI%SEl zS0!j9rA!}$Y|iEJgi#PQ_PB)tgr~A(C;?jOaTTJ83RsdsiNghzVCE1sBoThSWe4qv zG<^Q3Y%b@jEr~3SimLe+xO^|1G}JRLbwRhy(5fN>m&Y~V+TP%gZd_q98h;DRf9~!d z0=Hb4YS0N#OS#Df=^akG!tp29X=K$1Q*L@u%Ef^n|*`?DYpaMHJVi-Zf>a zkxQ?cB_LHKnDN#)@+UWU`QDAoY+|@65}H?jx{#}i%{XRU*Bqsm2S-Olslk;jdBsyW zn;f3?`77CeG9*%rzHiCav$fH1dE78Anp1QG{e=sZ+j^q>(dL1)wmDPs%m`9Fj-F3kldY~7G%Wj8T`L@?(wt30~Q&`r4o7FF$)2b0G`W|m?EDqw;E3Tker@a4t2GT7`Ln`zIYPOF<_&mtgq1B#lVG zED>WFiyn8G)}5VE!;Q@eNRNBQf)ouTD?7>Mn6YKfI6D!rGap?<@6^b_Of#Q6Iwz|m}e<$#P1ik6xspE|zHSTec890NB zlzz&GA2+J=pcQ_2_mIE-b6RMqGUG1gwq_C(0RuwesLO^fQ13Q3H&D&bGw zz0RNRUE)R)pzY95L#PLe@T*UL%Gyzd%#~`y=6H)=J$lHo8&!Us5qh4j;wxWinBY=Y zCsZmDk;6p?spHb5VYe!x6D|X1!=Unl^9;<#n&w)=D3uYM6 z3Zd?cT)!87(t0m9CaQvGrj_yCD_guZt^xglNq*nNZh}qSeIg=_szwI!kmV00{zSqU z43&R%koe~Z2i%^WATdyBKuuRb*Xtgx<+Tq8*}d2-Xr>8?Z*On#z1<0WqY9Fdvfx^g z#4%5UHsz}wopfM=$gixxAh;rh>&kkn%@~~y* z1w;;KS~Y+6&U>UVuP4A>#@^3vA8we9{`BS^(_oCNQmHHt?k_!Qlzj&wi86Nh_N8t1 zHejT7UA*})3%8QxUBf5hG~&df+*}Ao=Y2aL5<=Uy{Pn{_?)90j?J-Ma9P_e<9jez~ z-_rri3vHMJ4hMzZ3jX`+SGm$eMo~~z5?<)!K-7S|4hwr#p;oI#aHYby9+dB1y~5iY zvAjsnNF1+2LUGT~8Il7YltfFmjy}P08~MvScevX-y~}DsmGN`MmpCO*E|bPI=YFr! z=7T!ae|mK@^1)WaWsQYKp8KyRX;cEe2tCGDtoZicb#_LId;ZWp+#-fORHh=*h)B#i zV@4ENY9Z>mJ!wjb#bUt^@89RqtS6cig3w7W0xlx7*1h&gBkPbfhEZe@#1XF472m&h zjZs}j&KVgPoC&N0b8>q1ld5`EUV;#?KCz({k`v$k<~R7}WQ?k=PQ>%ZuCF|%rA{q@ zK%lcs7ZXIov(b0(_xJAcdE3!jSqjW$wQq2x&{7dpn8$@!M~+<@LL!)xUM`zZcMV6deSDrB)5A5vvSRXh9+L z5JF8$M^xutANb$?@l$Rewn(P+ zb&#bWxD5jrql~Nd^*Cu|@TRehs?xh6RfXo3t1%I+;wQ%s8TGNqCWw=AV2rZR8R1nI z`SAkwx)jn?(Sp(8;50fLu|GRxV-)%3<~FSuH34&@5;@Od?JOv2V3=bnx(IHw$)x_( ztUF?V*0P^#gu&ZJ3)m{zX{hj=$f1mPU#QYJn@D1gcoeQ~G<XF1Dog`4Ua=L6FB$6P7#0 zy=HQdl|SJZZcijDqsqCt5&7S#x)Q^ zAgGgYEWiGhr~vzY=I8q-{N&)UtQ2T|Q5~7+LZi-E_I+nZawH{E7SyMb63 zz=~U*s~5?_AWyYFFMPF=D%YEecQ$KwY9SaOit8;4>?TjsC#sBdj*<)^Ajya>JnDOX zecbWsqd9%myim3CTE8xpA*n2oZPkIdHUsZ&HdyX>-91CfgSf=G$}-&&&$3)9kju66 z;gw6MrosNv$!S5T6tOqJ2umHj=FY8Ebv!0qr-OyVf`SpizBCpL^<_-bp&d~347dzZH`P2ZBB?+dm^47*9Sd0v%q z_Pf=|iPZe`;XeQT=!m0PMlLu>--N_r!68O+&b+sKiM_3gt-7dxyS@qSN!eR*DO?pe zGYWKPGYIc)ZSu~}2BRpeXU4?dh)T@d8C&3_hffdY{P5mAtk<%a^8(d|yBAQ@@ag?W!lQY|-`@G0&rdSM zn(XPq_5_F`63wg$u2VGbNznH<>oEHEr73&kNVRPDe2`gmiqoDy2h(^`Eck5v;iYYM zw?+VR1(Mot*Pi_TrAus#137i? zk1X7D_g#%lqR~XJE@T(E6sx~ON&NWkUFNw$h_to<)=8t0vO{upXPYZA5PT4yHK%$5 zaL}S?Z9YUfNMQY_6 z8^(Lnz*NGfgwgx0`puv^CD)TaR542z9f__&vVLVAoCl=~AJ)7VUUT~}^Q&3UqrRtc zDfR;vpIPmkSM|J>&jb}QcKl$cA;*r96idBE1M@c^X}Aj^RK<GX{I1GiqP#Yl^ z?#>50HJ3&ip~kI7G`$hmM^#y1j5-4XzkGDe&y#c9jtJtM#16@cV1q4%_hj!twAgSO zn1*N?8bzj6W>1}45~>g0n?|PNiX92Wyr3N=@W^0(B?(5Pj9G%iro$h; zbz@26{m|WS0=w=$8Nije!Xn%WA>bmo5Ayl~K^%|5{vz??-`u9pMsQ;x!=jD6+3sfm z*%OThuI=p5#A=0R_x8BS&6|6DN{L))V7j@%WK^S84yQNaP(eZw)DB_Qw)K|z`0hPE zeRN0?r5K_Q)a`(OAn>9vB7(gs0vl+J{6~xewif2re`^P8z{Ak95 z6fmi9&*-q>`le1y&PF{_F4r|Xp+bfebSaysw!S`4Caa!}X+7dfUD1T1Xqb63u8+75 zPHjb{T|&ivoB3qX^Q(sk#kN~0M(9%XYLxJ-_iLFrc7dCc?So4rxZVic%?KC87?`?G zjQmfR@Us(!ahHThCbK;bd}DW;P-I@;A6k>uQkqCvo?`gR!{FNF?t_*mxCB~J?w(|R zw!e?WF(zOt1UHiMJbCsvCN`?TZqraDTa}hpkiKmT!utows&2xVg0fp}d>o zN-xhExenu&K*TaAncb0d>+%*J^lfo&iL)opS@vh7P*gD-^}ui&Ps=Ec?Dx*?nef^1 zF+mKn4G(}}p73?c&RCr*;|8~c2VdFdt>YAP^HnPi8$s9{*F%C0um3;a(r_8dNYvd2 zDTToCD=2B&N!7|S7h?vUi#|$dq8Fi(j;-kC!e6-)wXCvF3=tat~ z7pEGu0p9f3)WEbEqY8a2TfuSAPOx>#SlKtY2*DE@bzrhNDpH63ChVRV3<)v~f>>&$ zJLq!a-op6d-OuPVj3_!8E`lkDJyUOERc*M0JzQ^1s}bMW9I-d938uJ2!V~1N)1*h^_$9H@-bNqf{j#jOvQJ^EJwLwp@GP{3=dJ*t`jGrXWW&Sp^t-x;~EGpB1PWg8t5fZN8?(r1CQhNCkHxl zusN{deKB%4tw>vg2eZh>$B9RFj7LLKP4rDt^<45>1e)w=TA_Ppc55RF-=AK=XuWXT8;1Dg`KGTvaL9-*qYOl)^Z8qeG@DLWF554!-K4MNQ?@7`nH_pIY$ps&&* zH?+i^mZ$0(^BD|_%S9PZ-U8YeuaI?s)91{u9z5ctFZ>LbR8|nqU+8^!w+lB9^J*Y1Q^gh4v5 z`Nz-iaon!E@{1yhQpaj=vfx?Tk8gB)vLbO=ArV#~tS`*+MHZ)b`04#eJZdRgqne49 zF9M27i{f!&W>XQlSqX3NY}Q;GS5$ETU(ZV#YFg&t^nCmBHka$b$n}=tdoJlok)5Yv z$y^jKqaGjD)t5ZQA}w;F+O?_Or3k~6~KPbQ54n~0YAy{lK)tRcEQ27vNh ztqduCm1bl&PO|fp&mJ(7io9ktu45fvZF4fL9UL0``f$eGqeZDO#GW^~Uv&*4(hzi- z-S-0FR`3*l|LSG7s$#2gZqhI*0;OB(+}IjXRgS_ov%uhCQ@lxY3JO6n9 zK0=MHSKluTb0=9|ZFsV7QJ%g=%Vj3YF3M_hvYsxTR${=%MPj4oX^`i_ZG(d#K<9B zgcO%b18WR#vM`0Pr^u~|2v;_1w(I)*q~YRhguu1!9V!fucFUcTG(;v`pv<*gRx?!= zvzB|uha8zvn-*fqI-Y;|C|iqpki1(>PTY}ov2mJAhXiU?Dh(6XPZtANX^G06Z;c%CFe-RB(;kcf$Va6|y6F=QIf<Zi?U9-%XM%Ncd|t0 zy>T+@P>Ds6{0$EY5p^ZF5G8VyS{}?7+;8)0_heO7$a52NBO((qnc%uR4uQ+fh)aW_ zr-V4*&hBiD0yoADvGmhR&5Q{U&GLo9!DZ`CxP3I|K{nDrC@IX)b$fDQdy<6*^93mc z#01Swl(N*kt~3hfvn0Kl2q(!np0%srU0HNLFN~9vxxbjxtE>oKD@3x~aN+~%_Suoh$~z_1nThnzE} z!Yn&Ke{fiIG0xH2eIjv06T^k5PJ>J1?$Zdn(^19SlX3BNwKMI>_Bim?<`{L`6Y;5{ zlkC=n5aJ8DN~_Zo_-sDs;i5xKaCIbx^-=jE?MY`m>Uxe+UsA1TQKVY49nU^upu)q& zoQHj)HNkBy;soVIR#iM|QTP&}PtM2t3yvkPEH2M(3zsh}n3=jDB5}~ciNGX_PBpN* z(^On*DuOuKy#S$ARk1UwxK!5!FUx5n(%>4!J-XP1K&ZQOxR`UV?U{E@<%$mgO<13l z7oQZa9OuNn!$W#(r4PCW`Z#^q#mHvdJvrpzqNN|C17y7=JpkrHA{S>mcPt4HdgI|@ zPR{w69?8qG{s6j1caLajT$q6EZpOg04m9;>DON0m5E#{wMis@TX0}>^Kl{y|7ugdJ zj*mI&6QHM*@|@Q^gBLm~BApoz4i4#3Dgr7?b@z4BSV`7poP*h%;zu-u&U=p^(9rYoiutZC#IMocX!kNMl9 z4!VkjF_K7^o%#)!KB+>^JuZfektJhl#XUzLBqeJ=JoEDd<7iRic1*;GYejoppcZH3 zkq(0xNOe!0j`;cEghvTPm1u?*xD+9Q7q)haMRh!Q1wo=%ijg=m;#3JcV+nk7k|0}2 z(>voa8+C<>?790Cn26H|Wxis74;j6bgHL9wV#IRq=#aMWR~m&&uZs0ad2!_EbX~`1 z_a3m1lLas%My0dYKk4S29|P-T*l2YxTdX$7-F@CU59c!uyAwiPktCB?kMv=fFo_g0 zR|XK0!@d0j7AZZY+U_QgRUBs^h_l`(6JR$+<=Vys!T?`hn{2W(9ur-rQivTzovV~7 zXU$g5IU>sMXA?d>+Gn0R>SfXpd1^y$9p^`{Mipv~dgJFuM?9R(ae9#al%B!sdQwWz zm0)gBgbOHxDYRyCGXC!VA%|TiOC~2LNsq~z5o%}tHy~gJu9!yd93C-iyWu@H)E%V& zph9?x==DV6m;$?_D)7zi9fB0>l*{9q&1zIY&>?An#$|tFa+*Af(PA-eDTRuX433lV zujajAkIC>cjRD@? z*dR!cj)SnR#waMbO;MQ)isSL@xNlk7oCP&S1V`xl#Qk|kr;4kdqOpYXb?epT3!|1m zn$H2{ll>X{y>Og{oBsxkU6)IG<)jR&hdFsMg`Owp?mTmE*3tC^xT`}=BLUVTPvzQ4 zSak(Uj}((KzJJsuj+3qIMON?cvyGI_CXU^Juw4mbG4yiN(1gLp&3)=B6GIypJ@si7 zd$RXgeKV6nf&S$12s9!hqyhNUA)Bo+jf6SI-pK2>g84j6&dIL+&EbDBu;mkWWXuK z?OBOqg=r{yJ%}-_Bf2}8&?tcIfK5SfGYoSD1; zq8DKz>nO>$SFA2#u)Re7`Tha_^zewoHm@ADUh9Y6m&eR$W$P-q3iH(Q$>E$I-rL75 z6@z05q?{oIh-mrZhM1!c_h$kJtxWt(1{WnF^^F+E*WB=F-R<% zs#da>c|fFaH(Z?)6F!+KL`S72^oocT2Q@QfJrcXP((snb0$Nqnixxz<(<^^<=Mk1R z2B$SA<0KMjCR_)xc5* z^nAe`%TM*z2K32Tr1E=D^4rg1AM1F2L{W0iWHWSBVK@(uj``&1h<`plBIV(Ha6=Ky z187}F-rm0Q|NhR~VnFVWSAm5m>Wxi)apxZYGF#BLEh(kPD&FPC3++a|;nDX!T~2gQ z@L`_wc0N&U=oGjz*=W#9o107jh$qE^pOMmZb>rNfc}g6$JHB#!RRmd8-lQMsQ?;gK8UXTYp z&}D)!e_B{(9nX%GGqW~v(DtP@5hoa!Xc@ykoiLupHJsh;txa@$G>t%Jebdij=&1$C>K^55ojEp zxaaA1Ga`;ehGif>uRS^QT^)Sba$nj83d~aCpntaJ!+J~e1)~bM=OVzPiW*p#P>E!X z{D1cj(8-ulHQ~lcsBMkQcmLN;8L3pPE5N`pq6 zX|RM!G3eQSQB33PLXo?(NSWibUQB!0$Q0G_;DKzAg5k9zn(@?P)^Yo2&JXV#^W*yq zjzdF=0+I>(8j!!FXO;uMILiFpLCen$59o6TLq+wh7h(O(y?C^}ao8=vdsB@soHQAJ0rKg^+v!AaM#tYmZODyt&`?G+? zvd=Wvgpi-LiGFs15;b^3?@br0NgS3}rd%*Rd5RV4kh~~!6*P8D;QC6FiaEiH=5#jL zKWX{NCwJg@o0Hwkyt6f|@OrN~T#d%GIg?EYQj@(OwjML$p^PnY6i2@lJqjxnW)*S< z%Y~h&B4`Noc}^W`JZJjW=p8;;wEX?U89#e;%x@m<(}zHg5wk>8Wx0*1Yh|u4p^}Ac zMzT!TC+bmHajstY5rin}O$48UPA&pKmxmI5x?I%dJa|PqYslg&#ykdv>7rpJh$wTE zPfl9)`}-UpN4ky6d~>5=qn}abIio?rsW+!qC5sR4A%^JS5&I=o_NGNY#Vvy!GYei&YC zjN%!*@#2IbE|`{zjffyQmqkDoau5#s z1%LM-aQlZp<9j!D_`zFOxKUM%9ULP@kOUDSt78-pte7g7hv{WsOEVCP7;?nWY=~uI z1P_9lGS`az4FB-SUH-9A$F-NNtAicbe`9($$z7hBAXFZ2}9}b?u00~ zL(0l$^OmDe?(vH|`+RU^%J<*i!)mdj z?3N_by0br9@T1S}@zebociPNRHgYVxCPpL=qv5`qvy z%~3LDy>TpaKJRXGYdi9d=>}Jt5#vg!gAi=cqaNCwFXM%j5HO77x#01!W@f_eqXoZy z^ng$IXWVNOznS$Mr=FlnL`W&2afF1tdd|!YE6`0zjDsrGI)3|D4laq&$jsdB=OD30hc!?OvfX(LS!6tWvVk|rI-5oS~5vtVVT8Z!Qn~IqfYo} z*737@_qpATNZkw#M#RZhku@TfBi_*sZgNp#Ir&1-eUO|8x^`>*GJw#fyh5_P(g*zl zxW@8HB$`PaxXKrN*9=qy5nASqtl^|HIuEGify2IzudLG0C2*CI!JJ<_IOLO~6OMAmLPTsnB&o9L6b10K!pY*9_F67*ka*$y?gjMlf{Yt^c}3?VLx2=YR%@cLK>;xgzs zE`NY^;`n->G<2X)Nrkx3CFk~$@$l$?Q8>b@OodX1$VLc64aA^?VPex$hkTR7NE=XQKAir}=Ol)XT2F$1w058&JoF*C(A-?FK{&dUr zG(nizfV{6;ldpS3DU40GG6plCMd>7SXo_b+EF<((C_#%b&axL?gj|R^<^=<@Ver1} zjs2jk^h+4btA*?dcQA2;YJC%amr3KyYo2}35M2steZit5F2Ri#rGcxn0z}=0Re08g z#@D_iwFs`b3?#i({%-XE03ZNKL_t*I{yh-9oMemDiomLZ90nSyiuR{ZmJePwwvxgR zh}6SrB*v4_-1ZUkq08Gs!2uXlt?LsGseHvacbkR|eA{5P&#X z9u_b)k;F+j8{VuQqRg4uy%x|@`doZShGc^X!G~%SKTSeXKvXjWFJn2?fUSpXzvDRV z2F&a<;fwGh%G66n>6uD{FZ4wP2rX4crkptIDf-vEJk?ZAnbiSNG)b3}jGXM$pXSUa zsaYmxBaEtYziqHhG|)g0^g2ff4AxjFm`OvC3NlV))vcxU&YT>Mkk=&O>xTuQhveeCw*&B&US!Ogr zvVseh3_CNjHZ=NOw7LE z)qKdZLiDqZs}EhtsSKRCua?G6eR|=k`f^ypMZj0vaBF|PuSKOHPS#>bw@Hnd&{Yuo zjMNvi=gQ<{glIoFCw_tE;;gPlP#I*y*0GLtyb^7y4=QoX_Vf#i7`gkY4$Bu!7|Z{j zXVtilb*$rSnlwZRqNf|vFPJoRB^oa1Z_nnmGKwqNWPLZ*v5v2IgR=Up3R38Hp1;6> zU#gQb4O7Lq*kO2SFk9AXOQNW>psw!g8T>ld@wHDIUQEtZ_3>%S7X%ozkbD7HCXV#{ z#BmxiMuHTP(sit39j``vQpHfDmwt=))r*1jUG33mSGeZq9AKorFE?1YaPxK z#1uxc8XN-b++yJ3N~0u=3#*Ltz>*Nc3RGFgI@a-OR2s|o_P0tt?!+KD#XhDKA4?d^ z&sXFxF+(cFvLjmIx#w~)w%j?KUm4do25l_=@eYyFp7Z~0&#s$UjC%@Ty{PvLc#K+iME{;fXz?!qWX%zGkz{8H~c$+@EF?fAbF*W zFTz?0>O~RceZF) zmeB44&Lc7?gXO|ZTw*-yHR}tyzap$Oj!LZ{Ij^pDscPk!RmN~1lAAAFbP>z;o7|@) zj%QXIk2$}W6-hKgQ$?COaMG@8jhD(9;grtnx>Qywms$fzDukTXivueR1dNnl?X0bj z0imbQoQVS&9l#F5i-@&ws`arxuwJN=8->VbU7>C)iJBL+C95h!Bw@BdSVVIWwfo|F ze5Tqs`$ebX!}565A?7qy#pbByK|5dHnU_0iK?)ce+);PbGBHL*p=Q!lL_OUdh|9z= zbepgG+Tde{5=98e%m!&X=#tUpR1CT-lVoV)2KBh5TKhVl9d%GP#uaLrQ(0i;1tdR{ zyDvm!f$M%8PPaQRRB32okE+ND$**Fhjuo&@8ZQ$uI6))hSg~1G)I$%tIU2DwZrIWe zRiz4ns8XEPhNAMx7w%O~8cVS!GpDyq&P4@mp2p1jmV>rsW{K9EHXCi9==+|N^#Jd} z_C!@_>bl5S_+WW)@pW6~gN2A33)0513W@#z>LspmZqlfwrlmzO%mO(EGO#VqrMklm zq9#O)cm8qYgqRWvR!+20(Lw94MA}%eF$Tp#P}TlerXYb~i@6JKf~H zolV}_7_n0YHng~*Wr=V2L*L^%AtM}h#=SOiXWsGg{t>qy9`fl)4=9!g;oL#P)PiHd z!GxFuF#%ChRZiHL5~F0a)$9FyW|Xa9yd8~2sA;4lm=5D=!_R*|R|x~Lg?ZesJrUdN zND%b2H8~dyJu9WMJPp*HriyG(;rl&(s0r=_H!ek(hKl#M z#@O{MBy*0<_-Ox_AK!b#FAiqhUv%UIF&j;S)`SoQqL97Ev!kk*DvQBz`gO-Nst_2D zniU2#_#Itj%$bIqaP02;^PGtXoiQ0XwLaFKJl%1d4egJk!)Q=|?WSR8Gy;wWnpZ)y zb+8Y7QH=IxEZc4ahGi&iaArY8M> zR=5S+1G049ev>N=lht`B;sno-yCP;xKuLUOBD}M?%e{s1(f$d)+@JC3@r?aNOBE}6 z$yhLAcIrM8L`VS^RiMeQ`}NHrajQ4G3=O?7nX(fZ6PBLNFXkAeMzJ`o>uJ)<&) zd9uoQEP0p=eRC0&1WMN5tSWY@3PtE~T$Jd`Kat-Yo7WVFsb|6WQ9)EN4J=%_QH^=` z@;2Yv-sGJoaBVZtj3O?Oa4L^QBF_gp2kKH#^QA5xW?jYZUe(ncr!$L zYOv&x133&OpSN5V=j!GLKluOId$%XMt|QIwm$}y7=Ti47TnJvkyF{vW>$`epI%Yh! z!x6T_kB)FS!hfIN{9=2;9piR~rhB@jrn)H!YC&wU1(E<*0ICXgId#rHd#{!0hqbFv z009;?0tpbRjHn<938zk-+ACM)mtTJ0#v*TRZ1BqB7-deU9WzM`h>Wd9bHK=CGA=?s zL6>t3{T-hUXmG@oWY%AhN`ZoVX@;0E3z!!xg^l5W<&pEs@`$}@Wa;LNkIcE%cFeuP z5@s=Itwu{jumdHTu8@zJMy?f_P8|xc2#xj?z|&zPG`Kmx_B~aJX~cY zdizE{fppXJ1H_F{2&|22CX-8~9`TbqoBYk@ZLaN4I5f~$fjaIj&$~zL8G*^A2jhFfn^TBiu0@Gz)JC!!tAmC;b>7@GihCKKXh zHm6GoPFY+ldnXT?KpPpQa0k}M<6Z;IjuBb!0aT@e=X^UQw`1zq^78VC@4m3i?{BPf zvFeDOAytKR72@Snac%J+WpmX6o{YIxJ%B7HT(6w_gLM5JPa3HQg_Y=KccLWMg0*0k^wM{hTM|h2em4U6;kjDEB?| z%!|3zV9+UCM+o z!mNzIzBy?4KXrjR)TDffT19ZEy=E3>1pk^F0E;16h*Wfz@cU zpcX(IS`VlUFb;`8&5)^NE=y*06i8YejMS`rmhsV+t0M9Mo*|ZRdQ>(WA!c)?T}M|G zd3M1jpvPcn@d|!Cdv;@3Qr8;WS=&}YaX5Itb6F&K)B=#7NZ!_m2;!C4)RSQreC9rWZF$WUDJ!^4WyEBYNJ zjV%|L%rhw^qM5NhZ8==%7->BbvBt$4M^aDa{EYYGQL;En2r)*M7b;dF?4(R2oN)B= z=Yr%VCV1#&m@Q*21PL%}_~yk8{`+s9k} zMyxIWUlGXudD-xRN~1OB&f%QS40RXoPFr@H4lCQP6Nko1d4f90-IDZ#kcSh%apDlT zv{*BYrA#acx8P#la{_^oosmZ*!P4T8cg`*Gzr6DnuPqE&HbdH`)Fq+9iY+q3 z`fP9eX-5?zSTV2`jUa*4cGRik;z0TCc*y_!y>GJ?6NJRFRfGel?K0tOAe`-DWh46N zGIuR)l?WFWhAb#l?vSmoFXRD``4J%KancZx-NUZs*1;S%K}7<0nvR1m!kEs_D+ixZV8T>1A`7DkLhBzP*zYU=m(R^g=9>ci{sjfm_5TXT1A zPUa;sIm-4dG3hccVOx=16YCe(G>MbS8#wU|{J~12EWN->D>an}nhPRp`KaRf`rouk ztx)M>#Wc@(?ZOg2IKRrb&o9yG6tp6X(7Dr@9m~aw`Wcq_3`FaNG(=IS^IU2p{1MH8ZByqCh0Eh~gmd6b0NR)s}(L*@; zJ74=kL$%--IrHjJ`TqKdZ=N4fks*w6@t!^oRV*XF+!@)x!B}$j?TtmM`5uE>$@6D_++CmpInk;ZL0}MsSC>Z&2Nmua*RtPa z?&t~O=Et=s?!KFI&WCNo-D!&zvm|JeGim0elpxzCuqzTK0ci@7njhe8Je&kX1Vz|b zTwrlDrmCvrgBGJc7c6pZGXxl0;(vJioBZL%5(^7Tt4iIB%H1UyyuT(b{sRB%tyeKm)IRuH{j4U%qg7R@E8*OD zOr>Rt*OQEo&n@PUI?u;=hMDb}8He*Z`?IdRu80wEcH05h4-T2vy18Jwo82c(@Mf4S zCDjM43yykq@dBubL&z24%G<*!D`O*cBTV|OSH1>4y?3}Jv_Iq`fmo(kByo%ZE5Z5W z*H(Ca$ykpSajsMn$yM7E-y%wdP+@5AunqO^I5F#4h}sP`1+Q@#;JpvANI)1M;&fhY z2SpPvEk*uhameqlEtBG$khG*1$-O?Q_0|g3%jZ4qQlmmlkdTn z{fdG|Vva;nk{BM9;1wo9?U@{NGN%EVc<^1-uyt#m_+&an!vRUt{ipe0K4-g`qFHVx zw@t-%vwP!c3Z|#Bz&jEV;llDVOM{vtS9y2;TIlPG1XN>r2znZPhm|r;mC;DJI3Drt z#Y-%YhP2*cbs*=CcqWhfwGUH}WCf24Mfv=~$};c1{1VF{6QcHfrO;z^dVGOC3+=b} z4)9r&L5M}=P2k0)75HMlM1`B-n##Xhu^etnyhw1 z#kNFbl9fjN(d+UjBS)yR@$%v_tAkJ$<=wHYUkf1A{b(YTsZ=mCtXF@c%d8HZch*<< z=JG0I3Cz_oEfb_5XGrJQ9`jHq9+47JvO6nP1Y`oXn}ggwXy)9ROiBIL>G7-Wr_APbPTMLD zH-u7cn1&{poLXsk05xZ=8gOnWxfNbn95Dn^Dr4F&Qucq9n4Gv1 zyeQ^v3~OFp9^FxpdI*yh011FLO*~p!7^KgP082(XkHgVt0 z>XXxeP$iH>5LL!i#m2%C<9bkpd;HlSFK8$mkVooSUDq+JYF@qc0_$}o^ow~JFQcs( z$)aR9OB&BXv>3IPF?&`chh7WU%0yx%Di;bhcOJY;Q9 z_f_zzPHG9P0UB}H1>JO)>)BvuGG#JPeH)hHWYQR#b1ynOP2!VzOJ{-PVKX3G<4#VB z^YEzT(Pjl{@H}T|6)%RwcQ%HMER$j^vOJy(3QeXvy0rm)1~PS)qQe}K5uyZ%g#4frNb<;Q3-E)DWg2T)%8)e^ zRO$5TUy3<2RC!1a74NJJxU`rGf2tajX-_S*lE*aceB#<|)t+uR;p)8u<7UoG@(fjQ zsW1ljlSY!xy??c!5v}B=0r4ot{}v z)UsUT?$3nHfqF#TFv`4pFsyiEZIy+AQpLg9srp?9N`cX6#QMsZ%PWg?B3NH#M1A^{ z0yj{MNappGHC|j=L4pux?H8yrNJ*?Q4ZH5X>5{G|v;5CH+f3*7nM?70($KPxpQ*UA zv(0|fVu2g(X)|lsJuPWuy23W|NUEUH%i%chB6OXB==dyA&iM<&n`K?@_!1Gk!%X$MW}=I*|Eq_e?&bLh{sD47hV zd%B;L3uy9*PL98$~Tu633S-m?#Nl-crwQ7 zd}Cz{0fN*;mm)v?N?C}^wc#761) zM@b~|faT-U1CsLb`x#cs#bL#TVMW9cJ*zZ+*F&^0K*yEx`pObZb(vH^0nwlO^P3Hm zhVNgxz-U;w8Ud%PPu((4Knl*TnQaBp8%^hIP8#+*FzYos9+@<}Xeff?S@~$M;r3w% zAzqJi>?;4*Daz60!9%7>8Kk&}y}iE7g#nC10BZp&B&wsz;~61-d5`G`ft5kctBXUb zK(VkpqluqIUu`IX0a50Ku#>tVf(`&Xz3Bp5lgL{qL$r`$ShIOx)05Wi0GJ$M)py}vG>OGb+r z9?H&%Wf<1*?)oz4m&ObboUY&oa6+p8=|>L5Fh^Zk8AdKIEzn59vXlLcCVm!Wx5IKk zOt`!uzp0&eiSRGN_23 z9aQMa9NLjdl8B;gwH;ftIro|w$6USHXeMTSgHoei7bwIAfU`O@%q{d8!O9H zk>gV5s3kd8D}UOLL^#q;3@p}>3!@=PAxJ!{G|mDuBUXAWdvI={W>gpNm1kk%zBC%~ ztqYf^C`$O60gY6Tesd4&fbP1>R+4<`KlZG@zMrf)@(owgG zkLU2=!JJ{PKC0cX_k8qS&E5pJ#>gbMC4;qLKJ4lz3hdk`QC!e3e)n3Y80;D}GjKoJFii9UyZ|rhIF3j*&r*EkWupKWPaJ zb4RYAHK?*;%n)x`4vW00b6#0lpv9=jbhV>di$}1w$1Tbc^?{<`)nvEE@Lku@X5Gp4 zd(Dh%hjY|MBvq;|edf}N_(QLM}R;z3Env-n*uH5~PFU{sZ=su?WrHO+^a*^bhUw z$|B!iUuQ7tY1bZm*Kd0!7TSt3v$0YSSg7kBZ|N1v&W_9(1nOQaGkais5ChAD0fV6B z?0f}DtJ8>@q5)bQw~9I_mzM)?oLglesaFwG%pr?npIzI3xHZ`VZY1xnO*-R~$();q z(|+CGdK!9sOwuSGVr4;xvaxES>?XLny-&)L)%;p;*;MgjbCCT=tvsE)tsiweFDkq; zj=Z@#ri+dt1e3Bc7@z*J6jxl0m8xQ)3Pq*q2yd}7X|CV(5G_Ky-UEz5VWEzUV}*PL zJR>tBXa!m}P0OVgxV*Z`tILt~K_FPaBXV@C)yYSiA3RWbq`uPF6_G9Ay5#hW4|n&u z)fhPym9j)aUD3xeLi_L0s}V^N7|}qZkzZ_XF>4)3aGnU3#9J`Xyn@(dagvcn~w?+7J`xB*d zp|uR=1a*QWzP-N23(ErrZeT@ioMLEn%z-+wh5LXB(ovP+?$_{&pKtGQca|7g1kad2 zG=0!__<{4FJre9*gghL`Mwf+;_b2RhLPxbHBG<+9roe7M8g)%bI8~$vjSL6QJ1dJ^ z7$}uzPzWJl@@yQ-OlBN}fq;t?krjD9EEvw>w|qAI{w;o_8#Ao5n1g)G;}QynA!Qvx+>XVDrA#M}Or0OVGTi-|nazK4XPf=TuncWTWHQk!KuO_oN#n?I z2_@%D0BQ-n&0FASH*RoiGNHvj%7JUHzA5Irm^Gg7gDWSk8Xx?rBM}I-^QRYI=F-{< zIT`}gUOf{@!+|O&K{+;+c{rh-#qVf)qD6S=2=8+;Q3El^S5j$2QIbOJjLu;+2>jve zudqI@2)S4SKOVy|M+9tCV@Lm$OQm3NH`uJ z!w6N2BsG-Gz-u}pt=_+7x8@^0Ih=9#@DOKob=ZU(&4O%AL!OLNY(ylT1VJDE8hXo& zK;cQl3@001BWNklWf?G`$4U14W zisGn08(X+%@pPlk5rSdS86|KNh7`rSC$W6;aFzb>e|MoHbYK#>uvqa2=N8$hGNMjf z74e#|y* zPVDN(1EHVWQ`wx&gF|i|wp2-e)kt?OwB2S9*|i|2YKSIJMMHx?h{C0FE4;iiX50fz z!Ij`;fP2&$k&ENJhcEx8f55BujF)j1U+_8kDxMb+p-PHYHN?z?sJy#zo?&k?F6s13 zHAhc1q0!_M3A$yuyB85%hEXT#=?kq*Vd@WMRCqgU42h&@HEI_g2^tyH`Uu~$?fKi!-F@CNK>|33FW z42?qc%z7ofdw!kQR!2y1EI27TNg?&)A{8Ni#Ut6XBykqMNk#f)RWB!#$efnfSC{$D z`YM;#7nxf_Ivr0rzQiLwX~nl(!wt!DwRQRES37(B^X>u7Z@9(rg->Gi~b6@CT>T18;`9vJeLn7d+MhWLNw#TDDX$GTx3C==v_%KS}5G^rB(5-Sk3&b zEjf#4=@}6b1~%inm(KCpc)+-lqERXpQBUQGBw(V*t`%LJ>+W)8vuU|@FlT3$h`%AD z^$RBru|Ocxl9(jrAMf5{+JRcWg4lIFtGDrBcj6T$kR*}aNnT|ChMxG=#v-q;F0-bA z+MK|BlUQIfShtj$~ zTA2F*_NU=nB635_uOyY5SMKhxIWs&7;dFoZ|FWbJB;wv%4`k)%TRSvOhotnOi=EWl zcx1I9qGV5$Tp*wUscO_RYZ2bP^a2+a78sbI*^hVNMC2>s_Mat)vv?M(4OJzCz;`db zz{Vgj90WXNDvO9ma+e98C6}GF8fk#tgl~$-wV3n!ox=xrw%BblB*%UJ8=V9FO%9bN zW30-4p0V8-f4wu~qfQCN6}5cL@Xe&M3pA2bia2CT>R{poiBvgK<-%G^ZlDo-z`s5x zysB-SUaPmWOc{W_Z&*JHZso>eb9aF&+f|W0S0vC>wNaC2* zSQm*}ldWSV!dt`0pTD%mxgg{ucr_+t2pu6PMtwX$FiWMy7fXgW*?fY;Ejq48=2z}T z-}~X_KA%jLHqG&1WLWd?cIpINZ6$Y`QYdSvqbdwwR_Bpihp^>kKAUCY* zwhZ4j4Nd2kJSzDM&}>yA-#fR;cQ0??+V$+zOvjv<&Q9%FJl~NVYCzHy(ZoC_hN`^0 zH00lY>$TH60Sf-7s>$7xnKcd*lr1OS7>|Zmr(J$;t4aL)!(TH?2{S{*OEUaYFbydr ztIoVAZMm{}FyX_!13o_N$ijQ>`Dzqygdn>nOfu3qBvU)JX^Ns;s)cW@)V#W}L{$Z{ zmaY52nOV_UJm;aY#@ab*i8v}s{+HKQ`MvdJF4w1H4xy}0C6R!OCeS7*yDGlv-d*eF zhgY_zIe&O>&W-&Rs^NWm1NpLmP>QHWD(;GeP%#e`A0IY+usxv-x}CGXUkTSR-*org zAfdrM_3!EPe{xG`1+R{W{NROi)HW|$c2d(=d-hbF#d95Pno|u%NHt*E%vh;~Z>}x! z=F*t;FQ7N$0@^Zs|H=SLg1(xZA;%Tf4M_1zK_3OFI*OLz$^B&eO2s zBo*U*^V_7#ox?fT?j7>U{tStupJL#SDb1HOBHo$qWc zGjix0ES!x%&*J%xsDXKMG8L7;Yb!(EUK{hu`Z5nIXFU=<5K<13OvKN-9(ibpDHy1=ga%6nRNPT9DcoxrlctpMBrKO60^XesDA4G;^LMTgVr$h>9Ql|;dM8&t%xn@D%Z#4Ym z*IW1a<@SWz^A4?)nC1lQFe{Rt@?~KfW=4e)y$IAKPmqjC1M}3e7b-ru`6;`z86Ebc zE~P6vsBXBh<#M{~{E*1p8k~+80x8dUb9KPCR~J}YSv`BG&f@v5G>9yX2E4X5;@cP3 zSn*Umgm5gM?)+Y5Z9?`&EN`jX4YPFB+~3>lI(~KY7Mrt{Mg#NtoZy*wr0S*Nq~6Ap zO&U=mos_+h=z-cQTmzjrAqX)=e!RcO2Tj9lHe=+x$K0=)#vAJ0XDH?mZ_yb7QWYHy zGSOgVxWHQ*EBx82GpL3{iOezPnyE6>7U;}LAwGd$T18L@7BTCJvy?{EGQ(7#J=M=U zrqK0D{F0eVOO`n%a|j(-IyBEAXx}PC)D6*ud5hZVK%&{SUec>m5Gzq;3wrT)lcz7nzPoyARe-*xwiyQe-;+_!Xe$F)}u z>mB8>Vpeo+Jm#$zUf}!719I9Y#>gNmS&dE_#<3`k{nqc_3X<$%gb+w6(e~VwhIDqS zp7TfpC0RzzsVxvwAaqsvN&$~!$cf4tR5DX4wykF3)fX}d_rVzHGO=xm0p{Kzp(3?~ zp!LS0@Xq-aE^jO{tQDjT9n|p7G)D?Oyg^&7DXnWEC?OgHkVz z`JcZ1I(OgO=6aKG4`^`axuxr74CBIYz0{^!Y-MvUlJ6k~5JiKN&LEZNK04_L2t%n+ z7dkQ)0wx8=%N^8W#9BBo=b!HG^S2*u@h|&_B6;ul*k%G#!F~qc5YI&%{z&RiL-B0LEzvuc zea?UTjkkGuToEIr1YV6`I7HI?x5uG!ha$unQ4xYEl8tWOK5ilCES{rKJKCsr=9ZYL zF^@(Q5^YEfV^R9P({%j9r#t-Z^*da}IRO(KHQe>r3dc9Me>{)QHQ$eT+G{Lu>= zytF(hHVf3GRH}sEHU^>Ug;)cYL9$UxpFrfy`Q-Ue8s&G~VdMZY3Y9p~I%@Bjr-sd@ z<)_5BC~!RiRa+o6o)gW|qoshz7C*MsdiR2;V=q!XKWR-QvSTY@+_W%2n4bgS<4_0GSKCMu}S25m-yw~1Ae-7 zz(tuo!5i+X8Xp^?%w0-{sD9XYN&Pa1Wl^L_<#K0p2!QCXE4FElvN33Z#HLn zFk)#uVC+5-k!2BC?Xis`d_zS9^OEXCq38$~O;L40ObYp>66&0|w7Q0haO3uE63|IN ze@n;J?2Zg--afa?`clPGHN-NgJ!W7KYy-)U=h!m)uHmOwIToJ#q+#%eiaiZrBo~zgCZE}er!PQ z=&_yRiLZTN2lyDc5}Y3%#QMV@eSDkucJ{bCH3meIXQGzZc;=Eu41!AqF64n?*$|7| zJ=o)7HQ>@heE^uMt^&&#j|qY%9Xuc{*U+n zfnV*xBmIP7T7(QGaN*g7c0~na;V96U8tIeYPuK<$o`28*=76|J0L$Lw3W|@E~N({gD)cJ57iLhoK1bYPC2wD!2JLZn|9;j}alrzCIgD6~FU%8d$vtWROKfAZj&+gvi=D|U^;!kNmg2ZCM(YMxSErQ@0u%L{*<(-W+{`ln=d1-OPpwdza zxDm1;rw9a_TS8=_8g?^zQ)>M{n(qBUHTtWYhqJpsyS>fTojta@{$j(h078Fx@(d=8 zp3`{@iXxyYm_y1&lN=M_!s=39s@Re2i%zv@IS*CZD8%{%IQyBzQEVei<_!vFbLyxp zj|Ys^X>;b@;f#YM#4sQVWD?H(>fm#4q6AR_qLC&jySZiOa7NoY8pn)AP-#SSCR(Ye z5XGfNCBh?gOi!vxpS3qnH%tNw%bf9 zcpG{OrsZup^b!pu5kf2rVPdFczB#V>!0eH>sm4xH)d6v}sae=WxbH_x9LoC&YTp`Qb7n4dkw+^GpZ<&*_ml z=_ELImNcGj3?*PWnvbfn zza(1{f_a24v82u$>x=xm%P;cwxn)+OgS8ZhK@dXudyjrTjwdDNHgS`k;9L**&qVm^ z6bFC*-p#xG!<}9JW!lhMpwfz94q{|2TD!rXtRL}{Yfp|2h#VD1j#(k9Snpyz$*?z> zac*IO)u8u=s!3GG$@5qU!yXuoClbbS;)qg`g(0{@s0KJnO3tvVSq_o)#Sx;J?b(!R zCqx;R8zN*dJ@qCscW@^N)DnrIqElfnXLdW|*47>e(>bGs1r|pm#4_qFQ5~1~NCf>@ zXYuUZKGle(Br!}9ctXuFiQJlX{QUMd|M6Eh`PJ@(yFdyBH>(`ftzcQFkb$yf+&e@R zr$Ss>_-uq93^M<7E8B_FVE`T>qG>vxc@4qAQWY1%V=l z9gc7kf3l`dPi}g0;&6xvdg9-Y43AXH_TFC8f(8Z^tPJa$wS+M+29EYqjw+7(6Dzzo zhZa;ya>5F3;$(=$d}%=gi?IfQY1?qvbs+KdUpflekRx?z6(k7aLUbWSVcvD@wjI0k zj+qOc!`zG*YbqXZ)pwROo`DDsu1s5DbC&q0-5EdrbcbK=?6H%aB!v?cbD5@?Dyg4> ziJuOH;*Lu|#8?vJTdO1f_2n0MZEcb0xfnGoM2)BfaxTJFju-OceW9JBgmENZwF7Ks z%hyEndnW#ux0|&0(|Z$sbaR`Tfpn?35Q<~s`_JYuHy9#}gG7RSkltwhX*f$vGk7L@~T) z^hJqmR6|yW17_lEwQ~+Kr%tImhgvIY6R=DULe?^33n+7)Qj0QT$hPmR){4#(3>LuP<9uh3!^#I&(JDU6#qcDkO%2xwkD&dC{#x8}6aZN@4A!+`r zrfojCwsXM$`_V0KcQCYyij3i83XYLg!G(}Yu5*2|nA#J?G#)UoJvux`qe-inix4F+ zOweSw*|zLVr@V3TVjg05TFr|tj0YZSTCY9EI)xVJFa{qOE-D&vDJ_X==zB;dSO=k^ z#Rvi`D!jh7%!TCzwzqE3q!E_qSTwreXbucK;uuX`Myu|IseqPSCwTceS~Dst93@Mk z^eRegfm@C7qffWFH=EN{HA|xrLt26-R0B>;vLR>#W+>iIUFhfY5VMo3zO01ufJtF` zxEfg#>fyOQ)P92Bf`7JRt8#^!m1%?;*;_JQIS>IlVt?H4V+<0?C_J|PC`|NY#V)a{qlC8IU6c%Q#LBlq!UBIR=D^LIk@!fdA&jRsOqI zFY@xj7%1*&AvcIrg;m_Q$s%PLO;vGG#54&Bm$s2elhEB{vgtwo1xf!<+x&;Kyzs04 z^zn?pytTufE+J`-;CQGC0a3KHNIk>AOYV5$Ap8@bo;()wfC5NLMqMxwT6a>@kmoHg zERJSXjQiqg*~nuNtrcoZ9;qtS^syWcdhEottoD>O_k^f07!5eTvc%n4qVtSpqs|I$ zG+Jz?hq=P!ei^#0IlS2oY3q%ViH= zpc7*rtndfC$AMgW`g|b{smbxf_S_~79=dS2FJ=uek8@0oEYjmgHB}g(1X63%RpDGf zOQnulB-rVBvuHWM0y?qY7OiW@fPJUvBh9g$q=iQ11TBdN4p(ap=fUDC4 z;~V1vfBNQ&{N4-eEJr2uAf&nVgT&A0VYDF;gEW%7q0?Ov-BgupN`4>femJ52`Hyej z<{$5Dad+B~aw3G1gzO=n(3jrneN{;#kjWvF+=wWmk*rE%#zC7{td-?@w6~zrWYVzH zR0oN}iV!X7k#YQE6Nh_YpY|7d!6ZlwJ+m>W8PtL7&VFaKxqy~2YRS|l1q409VNPHZ zZvKV#S`NC*Bnvw(?3l8bT4wDWrDYhMr~{&bri2BjtA(UO%*ujQWw@i#BkFQLVM!=t zHF?fOJx-3NPq1i@+C7tmilS11DP{#=CrG&*QbWKMaeJR%Y#;K`Zp*D% z%hV%TV<9f5QuTRIJj&?{k7AfPfvSk{;+VCJBD}b^#J|0`%)1v?I6r`39YI22?ij=n zJ})UHfUIq!CXINt8#HX1_%+DyEAm$uKmFNm%inx*gI~`(IukS-F;*m(oMxV_@{r|XVPZ8vjP-pK7w+g7co}V-)LMabVQA`$=RCBxHUOsCnq|qXsu@4 zHb@n)y0k%chCvGnkEr<%wYW!^jN^oxfjvw(`seUg2qUS zV8K|f;Tub1{&-`9KYD4M<=_NMMM@#W($=Wf**YFpg+~MQNlsOgF6U-4OcbOP#-0k^G8c4*09)lRsPj>4BK!u%&=JJE%=e{KH9ONeAMYLV!S;lCNYsNB)~Y4yq}c!8-rGLMab0Pi zzjN-*%qkRKN$^D$CDCKqt+v%|PusIQI~zMQF}ty`U*^kv-9K#qjQzA58!|@lQInbX?G=?kbx}F*cx3rW3w^7bz_I!$%Nx}!O?tyyCOc+&sxUd zSHj0nNuQq@Gx|t&$N@As;*KX+0CNre^yr8m?;r5?{yq=R&SO~07(!YJ?`zWZnVn-^bTTN9&k zV9}KzmbuVr@iHS7RG;j=e4^W-uU_cl)rg@kJleOq|=Hj3(Br~|qre#NH+ zj+)X5NuDqn}t!3hCm0ahz1WMWQipsTc7AdEh@U??!GqdFyD#|NGG)f4@KFmk(zg&NH)6F%Wu-#YQMMi{KD?rys5s z%R-Qd5(-yRibB@2Xx33P6X|(%cbk9n^%r@0(y%F+wQd>B3#U!kf&@Xd&aseP(weY*HNC^rT001BW zNklM4J4pcR2B#ZIVAeZ5HFpU z;Dv)x+Om$-La&MNjF$G5orYJg?C|~FUH)`;gY}WpNuiBvDqcX24JC>}?u*_=tm#u8 zx+yDKcy}>i0Cz<5o0`*W3EsGSJmz3H@>x<$vWkyD@NNouG> z@zzIuQ_8x^n|QJJp9efehq5nvF5eWPt|I?zU3hV{%Wi$j>x(1q5?Dwe=9b_+*#jO% zh~*V&O+&HF%{e2kR2o>yIA&datf9#k27q&jazhKwfoV3T$478{#MVRQ+GLH}Tgpot zksBje3q7L>MolQkM}=U^XlY~|mqOdOA&%;WO}NaELI`vhT=Jw)}jJ7Uk@Mz?2S<`;xSTSQt17L#Jywa45xZL@67-GiTl#X9lP4JUneV zJf8B_S;NusF~{>$5`qN9ti0c#p=^3TzeSl0-6yY#be}BBagSO~zYiCG?*s{m2i$tJ zhO=DMyGB%ml-Swc;(y*!p1;1$&8({5ofBE16KbaoWh6+uI8X=2P<>9r%kgk{!CGy8M zApG12UlvTEpxSlS&?V!&`ws|zz5@`D$4xxPN(zrOb&KYMh-Gz8`z3O%*!=mN!!N(y@vAXX9aOqb@! z@2SCf)MpN-Q{Fy0psAg!qnfX;ZSwN=RbJd!^Y@oct2I5NF zB;|8O>v9;v>iW(Ea7CqCRRm89965%STK6(S%6VeI%!+hDpd8ckybFPaTadWb>L)?c(}-XIP3WJ!&83s=!^$#rgdRrkLU*tSE8)O7FOr>S#o=+LI*5h z>64>c^0(whrG06?CZVxFtRt3Zd~>Vj-+%ihc1D45RS_lQeP0NGs-HHU`Uym2%`DAD z+l9uq@^Iie?@9E#LFLWZFJ5cwt+#&i-~s>m@geU$JR-NoNF%*K7X!gRKZ^LW`+xk$ z&q+NSzhKaf7W635N0BcJb%EiUI) zMo$Zf7h8Fcwe^P8}ZvC6+6 z`Kptm#9nTYWgY=@BtQs(YgOjT+8STm+~9@H313?qbEOi-?o?u&#q&%-yb#rj@zgR^ z=!g1uc%NEyEqy5&knO4T_oca4uNnNjNGq><{{ICN@FA1%Pxymo?J;wr)VM_2tW4Vl zhqE&tou2aMNzd`@gwsWbiPF2!b_E>@T0x!<-LOH;4N%u*6ntjT$gx8EOvna7$-(K} zO1WaNh{kC6e2Q~>bCZAh+%A7|wP9;wRQk zpxO7vQSap3znR@$3m$&)H+!f2c<(;%9?zKf%GgI#QjEG%Erj&>2^yc9Ory(*Q7jGF zY#H&0Hx*|V=-R~T^n~%R@ALg@TZfx#@&8fvR!Oa8+PbO}W=pm6i=Qh#39>yc+W=_d z8aef><(6;N!u6M~^77U?|Ly)h_j)DFDiBAaAe$Nw8aQA2ol<`t> zWP@bF#|e(Rj{Vt`clVFjjFF8xvNdWLHzS^}I>zHM8{-k9x}pwc@TyQN9ZOXiB3vqo ztuDe6U>31(K`w^ArJ!LcFYK`ZUxGv7l7QlxhUX2h?*Y6B=5}4jK@v_D3#Q!(hl`%W z`GV7AEV9#fiKAT6_Cl8nZ^l)S8iF{Pj;J-Z4Az6y#FICH&%W2UWE`m!B@?m?U8)k4 zHANOS@zMeuBVkLg$Do@YrGm! zcXMuvGmaI(w|MWU zpJ?^3j+OhM{~$wpUrMy>MxTd~@2)3dEOO?|2Ba@Lr+rV`C5~I=tm`;VJx6`Vbg`h# zfP`U&;}n8I6-wYqrCtvRVor`ucE|l(KXw>X8RkPidrb`HmC#Z$xUnlEsYpSXi*bEC z;^yco-`=YE&dwUU>k~3g*Y}JWW3?9tKF_PpTwnn5&7Yj2zNm%K0@Jh99gGO~*3fq( z=hx5r{Mv)e;kzdtuYGvThk1eZW!hEE>BX@CjesU2aTzo|v8ou!T`r`UL1VNUu8N!t zNs&Ek`C+?{+}!1#U*BK{{?l5lXCX+N@TCFM&EX|4mMXY=ya*0HRu2PBck(hNIl1}-^#)!AYM8QQL8p7M%2%vz+M%z>dMi3`)mP zMT*{CfXc>L8eminc=Q2tDd6kUs`FxJ#ed7v1Ym)}0!A9}DzKn<^=_zgbv)*UrsH3~ z_;tRyy+NjDA&ENFR2g#Lu}B32fq}%e3|gKGon7M70>*6UkQ_vbcd5hMZQH$GS^Voo zSkFIsIOV_HJK+7}1rmpAe;6~WtN`{|jI@HeFENcLc1=b@v9~j`@^94vd@F6wU=TGB z==sy@+x)w4+~&neh1lY|LG)h&-ve#~w}w*XqJJ!dLDMHTgox1Vf?C%|T_IIz$Yfg# zX`vz>xW6cNlRx@!pZ$493=NG7vglaUzh}^JSt0(*yL&WfToH5HdS1FDzEoqXiqNtZ ze4&knuDtBps7UG5424c_Rk=IL08pBj)R&?npXEGQDDsKG(ODWgQZn>hh5$C)VpSI4 zGW=dG*oh6V<3bzK3Tuf=o$W!~#@$z?hYqfGmWvmSst)>m{bS&(Ezg}6iwX5-Xn00N zIxn3Rg)9rU8{xY*uJY>5=lGNDF+GXOjggh1xIPLZMx8SKU{1cob4n-8kf2NIz>`*q zvkNWNyV(|RYgPX+6aH@hH1qnskNNw(k2zQ*VyKZm7gxa|i$TbaI#>`ag67W;Pv?)v z2z}>av`5qUCy&BILOrke zB_$dNU2s~>U={AM$Q=$cagd78y*jmbR95COij$WOWGBI0Q0)pUk{2sVZGuvjEW(DMCPJ3tx#4O|Qt7OqWl%2-0yGRp zSXrwJC0PL)gJwiBPB)13o(Hn!Y>=-y_s`f={k)Vojk2@W@ckRt`QL88z`wY$$#bJj zB%rxSHx^NGr!m2^(?z4Uh$JhP1fm0vW_i3U4I(-%aK__CM(+UsAj;o}$v?h-c+7wL z)d&3OaKWa_(@y})mq7!DJ9?p)2cgWRNJ-CK97w1~)B|Q< zQqc8jn>Y;{tTTHwnvCzct-21{x)BI8DQF*T>9c@{n24yA@h~Jx87a~Lrl^NP0P>~0 zf)tvu7?nEfQQ4@K>l+iUO-3~R8E1W6^d2muV#8FVkr{@UT*w1WJ;*Q^XNAX*MF)C) zIR@_M_tfI$DX7W-RGv#I%4#B3FbTL6aTi|+I}houoX5~IXL0e5TyWQrbK;C&FmYL8 z2v0o9(~4S@4+rb9jlKE}MbK#3s3~3}oNHG~nV`_s>kg0MJr^M-AB?MV@h%Z7ba9I) zMLthOF?EbQeEgyUFJ9~dUfkZ~`&T#k@2;)!=T}EOztJ#`#b-p&K~=tdjSN(66@rJ7 z32DLDN|sfolT%9u){Wpj zWFzVXAEX&{INv3!^Rfp%XA{n!pP=#Vo-ARoFkmoH%AV-^jQP$NyUZgSrYX&jM0lZbqGwF5j$(^Y&Q)%F)#~fRE^S!mX41{ zG*;9&p>L5WxE3Bo8n6l-E{HI+Ge3)5^;Mvee`d=xI2nN)OQ7(oY;4V#^mxd{EZm`@ zLS2z$G(p*!jQP%b;7_kz;oH}*a(io&YvVDEhVv5cch@UPfQd)~+8}mJZ&NqDuT{8f zY5sQ8+uv4U^p_u6#5w-QnDBERVIQLkSh%iTb~^>zuG|~Qlu9}bZ`v; zA(xU4r%nYt$BSuK#Mq3+i<>pyy0*ptdiy3X@2qoe6bqx%9QOh-YK*_jkZ2lyeJb=C zIRlOpi-#dL_k{3{r2J;1yoQGV@|$_z|LnNshrjul_a2=xO`!GA{`QOvc-zq;-@LiQX2|sIDT~z8Bh)e?yA9wYs15BkDIJ?< z&6a%CgGS+f7|^%|MIP`^RI7R=X+PySqq6;2CN7Q zt2AhtKf!*B;dl^{G?ZUSM3(r)Ltu|+A1ploOpt$w+5g&N^OJYyE&unO_xaJikNJ3! zIrE~&p{hvkO6D30U63!(Y|$4cXt5|f)3+@D0+=V$E8Y+&v%!kJ(9G%VC&^X^3 zkALZsT6gKpNiMQ1opt32ajvEJsd9p0&~!AXh^xd3(Li#?%?P0(h%=U!bvkZN8g5_P z<<|BUcB_V&_w>0V0j@Q8sl_9Lq|7tG|I6}3ijG8?Q(0!71>aJMTvg$fYghSKFMW-F z_53yd?8=1aMoOazmxRXRjcTHp4>*vO>?95u%CjXZt@|%eMEmifG z(=PA7asPn-{OkMt;<#n5O|f4Lp1h)fGNa5aM;aAmWi>HFm8%TDVcDmsbFW)@WT3p z8*3x3R?3z*6D^$>*MiS@4vz(iPqRb#s;uQNQZk|lqTsGWS7ZVaVlpE`e}7;E zniR`CwZytK^lc_Zhmf#E;=OLchqDF$`|TZW?(R~r)o+N%p12%1J~iyN5N`=AXICC0 zgsqN~7qJ!*CtKl^pbB*n%F$#*tqW_@ywIPE#XN4Ag*dwtWp^~@&##SX?K;0c>Unej zh+j=l_=x#sWcZAAR>2ijUfG`T>a{JtvAxb_azAno>~;RiRj_|Dh1*f`X zH-Kdj?FYPe`G29PX8W)?baCNW=mmD8e&%TT>hp&DB*^sGb4+Z7H!LaME7qqw2_X(Y zQrz);#Xfa+DpNct3(c4(8a1v}6}PuGdGYF1{&Zq&ZEP?K6`nFndHUq`2g%6PdXj3g zsECuK&uuVmCFm?G2O(DnDWtnu`#Zt%+acGlpLjg_+0l$QKDy73_s_mMrQ%oRgQ~!X z-5IAJw|q1|=6hGJ@bcE?-&Kv=OOozc)>os}FXMhosCHwJ?QH$JkVaLsQJ)`ciN>eR zX8c}3Z8Py)Jz{t3Cf`rOqb~7ryWpd<6An*L**_K%#R9_I$pV?e26AyUH-+;NqzDF) z5$}f~cl%{GVk-kr|)>4Pc1Jf89KYypJJpz&o}Eeo*7eAY8> zowJ29?ToK)k3PIIiXS$SyOQ~yXMZKc>b9BR!gX6B+l$<<8@RhjpRS=TdLB#st|S)W;CXOXR#GEr*hG8+=h<{h$*Fx=S- z_tO9`rcDSz{hY3eJT-f|$Rjxyx!$fX15CLjBD2Fh-t#en2gFS#*l5hgJ^3e+CRe3M=P5N3TJ<<{Y?fUP zlG$GH{_aG>oh0^VpLyfaY3A;1&W|5XxpREVbODPTiRxdOpz)O{Pm*LBsqiFZH(~?V z)&k$&+2T*GUg6d#b7gIPQlh zNyfu~&AFfi@Bk*H&~w^%Oxuow`GSv5=N!y?rd`LpPjp_9dZW*20AmL&j$wn(wOtA3 z2!1a3Gknj6c=&0bL-YI`EF380Cg&sJF;|1|kE5a7cSXu5eh2|1(P&`37Oqs0D^10< zan05B2^-@PS3?|}>@uEHDRq|0Sc5o@uf&CxSxR30{_wlmR(=AvNTF-RWGXpMop_+i zT|C{bEWKS}uOIgDXg~G5Gn@0%{YSiiFs02xs7A$@EBBW{De`!VA$`wC zh0V1w|Ki%1fBy0}xE6$}ojpH_^1bZxN*2A1#G5MYy8HIfq8(iTOXG!d<)56n6{|rn zjFAg^q-dFiCWbBv|9}zvx@|d`cO1@J9xi$wb;jYM=crAbr5?{V{7jirMZ(Ie zp#+ltGs`Vo${9X(OFz#pvL_l5Hmb;0U9nSD?9?N!u8o*94KJ?sj7KBZnh{NisDWBu zwIFk}0wRkIS%jcj5D$abM;e+~OS*ktsd%~jZyPi|4dk2|c?uke$i3j)^~5_8!keAy z`?J*Z)^CpZ*~0^Vc5=i?R%&S&Nx&@8=N1jkWzhK21r3Cx(0O0@6%xRTbzTmUacX&P zz2@83w)pO?U2aZlVn6>z;P;qJrG$AsIkx{G}#XuDT+jSs@Vh*H|5l;w-sD-x@`+6lToR$K_D!7CJ2#o_E zYr|lktwNG&5fNU#7xz54&Mhqnih|ZN0>x8<#n2HsP^4g$jU5u`pKc?v^L;0~1t8pcJ;gs|a{UDIM+`x@h=f~wKVnwN1mH|a?q!NbXDMV4k z8bXw!)UHKk%RCK=*LuezkYbc-IIcO+%Zr=q?_F)|Jym@Pctu2B0d5b0 zW0`q8wQTuqk9DhFd^rXaSB0`^R3SKdC`n42(pIYAI*8W<($!6>=l*QYyz2=b7-`W2HP5ROQ9NK;6?*}{#5BHgOE5$u{lFRwZZwv;wzE@b&1~Df;+-U6+Pw<11EQq6+s3k@*5Z9EK zCKG;e{U)#OUSYEW^Tmx``d731?U=(Yk#JLqH*wtt)}6HOlGYI$S&mVhsH#!1IJ6F< z4%(6#zdQ+_b)gQ}#VY$zNdqXF%sq`Qq~3Dsu@;e5gUm!^hUKYvF669e_Y&!@ICp}M zembY(7mtqk`Tobed+?BH>N!b(L>j3GsdTgqa1ECy;WB7E88r$Tp9kEa82;pf(u+Y@ zV}!G<&h7|qZ?Eyq>pQ%(xz2i!35)s_LcZOzxb52CRE;QlxVYgClHDg}b-sYce3ZuHA3i6UTw-vc<#x0OrA;w+DcFk}z=IXF!`I6mPd_2eLo(uhnRFd8o!t77C(x^7L<%T025 zQ9f~nn!(Vz6A|K|S!(8p85%MnD3&6N1?=@5`F)0y~QMD`j9_cZC<1^LdYFOD9jo}+Hg$0w0{$0z*y_>6;jPn!nclAx$& zvUC&-y0pDxk@rEf5R?BFavPVK##e6z7!-?FLsX@fUe>b1YzA7~;1!|>6&bq3q6$1e zUgJ+T*Z9uPI=6S$nT!;ZOiy}g!>X5oTkg4VJyf<`!nPsn=Dr?9FLqu=PYEDjwoK$R zg9MLrDpD`Gb@w)^&Xh3oY)dlMo`~%AadB6Z-_=xom_2fwoe$>=e)(w12M-VU#Zh0n zWKtv+vVv#?6Rb~!cs|eLQaU`rpz=T<&`ZADBbPzrNg&h)j}3p!h(~;&Ge;%O7erC2 zsv^6w$Q@xo^G9A(#d|5_rkz^(=GGeDzP`mv+Z$|!4eBV3kZxOUZ;ARXQQb|1-QI0` z?7Hl_T=b+}zYs)5pAtOc=QxW@&-nhQwML(O%#zwn&8INDU8KuuE#3L$|cb&^Zlmf)cYv~_f55@t9vcRdK2 z4idKK=$=>MUY59@v$JT6>-?FIIB8oRoSw0FHs_;x%Y(C)lRnXlP!m64@cl)S(7Vhu zF4rgX^_7Zxu{Ts4bt}H6Zd}=LzP3K$TRYo)ePf;LqncX7%GHb9PekMn0%wBT$U$a$fZlal%)VR`LR2D z*1|-sJz^fP-g%gN=BekyvjuM*W|~m39+k})*{Lgbn&xnOR3A>7ny<+ku?obfrLE|> zN#g5Z>xPe#))8zN;Xn6AU6v@F+EX{!a`RQxgBIxUeJMo%+Ma2bI9~KDvN6v+^FFa~qcf-L9rrRy?@?*m1gcmNvZrD>AVwah lmY3y;eTrP+<@&>|{|^qDtq3xZ!qNZ$002ovPDHLkV1o8w0+0Xz literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 3183f8b6ce..052b17f99d 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -38,11 +38,11 @@ Row { // Size height: contentHeight; width: height; - Rectangle { - radius: parent.width*0.5; - color: "#AAA5AD"; + Image { + id: userImage; + source: "../../icons/defaultNameCardUser.png" // Anchors - width: parent.width + width: parent.width; height: parent.height; } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 7a48619550..81c036034b 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -85,7 +85,7 @@ Item { HifiControls.Table { id: table; // Size - height: pal.height - myInfo.height; + height: pal.height - myInfo.height - 4; width: pal.width - 4; // Anchors anchors.left: parent.left; @@ -195,6 +195,59 @@ Item { } } } + // This Rectangle refers to the [?] popup button + Rectangle { + color: hifi.colors.tableBackgroundLight; + width: 18; + height: hifi.dimensions.tableHeaderHeight - 2; + anchors.left: table.left; + anchors.top: table.top; + anchors.topMargin: 1; + anchors.leftMargin: nameCardWidth/2 + 23; + RalewayRegular { + id: helpText; + text: "[?]"; + size: hifi.fontSizes.tableHeading; + font.capitalization: Font.AllUppercase; + color: hifi.colors.darkGray; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + anchors.fill: parent; + } + MouseArea { + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + hoverEnabled: true; + onClicked: namesPopup.visible = true; + onEntered: helpText.color = hifi.colors.baseGrayHighlight; + onExited: helpText.color = hifi.colors.darkGray; + } + } + // Explanitory popup upon clicking "[?]" + Item { + visible: false; + id: namesPopup; + anchors.fill: pal; + Rectangle { + anchors.fill: parent; + color: "black"; + opacity: 0.5; + radius: hifi.dimensions.borderRadius; + } + Rectangle { + width: 400; + height: 200; + anchors.centerIn: parent; + color: "black"; + } + MouseArea { + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + onClicked: { + namesPopup.visible = false; + } + } + } property var userData: []; property var myData: ({displayName: "", userName: ""}); // valid dummy until set From 9b65a72bb58339b9586902ff09d0bc2be7b0bdbe Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 20 Dec 2016 13:53:26 -0800 Subject: [PATCH 17/68] Popup essentially done --- interface/resources/qml/hifi/Pal.qml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 81c036034b..0bcff94739 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -235,10 +235,20 @@ Item { radius: hifi.dimensions.borderRadius; } Rectangle { - width: 400; - height: 200; + width: Math.min(parent.width * 0.75, 400); + height: popupText.contentHeight*2; anchors.centerIn: parent; - color: "black"; + radius: hifi.dimensions.borderRadius; + color: "white"; + FiraSansSemiBold { + id: popupText; + text: "This is temporary text. It will eventually be used to explain what 'Names' means."; + size: hifi.fontSizes.textFieldInput; + color: hifi.colors.darkGray; + horizontalAlignment: Text.AlignHCenter; + anchors.fill: parent; + wrapMode: Text.WordWrap; + } } MouseArea { anchors.fill: parent; From 0be4db1e68ba2fecd8dc090aa32c5f77b1f0dc05 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 20 Dec 2016 14:28:49 -0800 Subject: [PATCH 18/68] Checkboxes! --- .../resources/qml/controls-uit/CheckBox.qml | 2 +- interface/resources/qml/hifi/NameCard.qml | 2 +- interface/resources/qml/hifi/Pal.qml | 32 ++++++++----------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 544609b478..a3050df22a 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -21,7 +21,7 @@ Original.CheckBox { property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light - readonly property int boxSize: 14 + property int boxSize: 14 readonly property int boxRadius: 3 readonly property int checkSize: 10 readonly property int checkRadius: 2 diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 052b17f99d..56fa0521ae 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -67,7 +67,7 @@ Row { } // UserName Text - FiraSansSemiBold { + FiraSansRegular { id: userNameText; // Properties text: thisNameCard.userName; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 0bcff94739..3a7c3d45b7 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -172,25 +172,21 @@ Item { // Anchors anchors.left: parent.left; } - // This Rectangle refers to the cells that contain the action buttons - Rectangle { - radius: itemCell.height / 4; + + // This CheckBox belongs in the columns that contain the action buttons ("Mute", "Ban", etc) + HifiControls.CheckBox { visible: isCheckBox && !isSeparator; - color: styleData.value ? "green" : "red"; - anchors.fill: parent; - MouseArea { - anchors.fill: parent; - acceptedButtons: Qt.LeftButton; - hoverEnabled: true; - onClicked: { - var newValue = !model[styleData.role]; - var datum = userData[model.userIndex]; - datum[styleData.role] = model[styleData.role] = newValue; - Users[styleData.role](model.sessionId); - // Just for now, while we cannot undo things: - userData.splice(model.userIndex, 1); - sortModel(); - } + anchors.centerIn: parent; + colorScheme: hifi.colorSchemes.dark; + boxSize: 22; + onClicked: { + var newValue = !model[styleData.role]; + var datum = userData[model.userIndex]; + datum[styleData.role] = model[styleData.role] = newValue; + Users[styleData.role](model.sessionId); + // Just for now, while we cannot undo things: + userData.splice(model.userIndex, 1); + sortModel(); } } } From 630fb8696f8c32da8138c4a1731e53ca0e2aeb22 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 20 Dec 2016 17:18:28 -0800 Subject: [PATCH 19/68] Lots of progress today --- .../resources/qml/controls-uit/CheckBox.qml | 22 ++- interface/resources/qml/hifi/NameCard.qml | 168 ++++++++---------- interface/resources/qml/hifi/Pal.qml | 13 +- interface/src/ui/AvatarInputs.cpp | 1 + scripts/system/pal.js | 2 +- 5 files changed, 100 insertions(+), 106 deletions(-) diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index a3050df22a..aec579755a 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -23,7 +23,7 @@ Original.CheckBox { property int boxSize: 14 readonly property int boxRadius: 3 - readonly property int checkSize: 10 + readonly property int checkSize: Math.max(boxSize - 8, 10) readonly property int checkRadius: 2 style: CheckBoxStyle { @@ -32,21 +32,35 @@ Original.CheckBox { width: boxSize height: boxSize radius: boxRadius + border.width: 1 + border.color: pressed || hovered + ? hifi.colors.checkboxCheckedBorder + : (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish) gradient: Gradient { GradientStop { position: 0.2 color: pressed || hovered - ? (checkBox.isLightColorScheme ? hifi.colors.checkboxDarkStart : hifi.colors.checkboxLightStart) + ? (checkBox.isLightColorScheme ? hifi.colors.checkboxChecked : hifi.colors.checkboxLightStart) : (checkBox.isLightColorScheme ? hifi.colors.checkboxLightStart : hifi.colors.checkboxDarkStart) } GradientStop { position: 1.0 color: pressed || hovered - ? (checkBox.isLightColorScheme ? hifi.colors.checkboxDarkFinish : hifi.colors.checkboxLightFinish) + ? (checkBox.isLightColorScheme ? hifi.colors.checkboxChecked : hifi.colors.checkboxLightFinish) : (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish) } } + Rectangle { + visible: pressed || hovered + anchors.centerIn: parent + id: innerBox + width: checkSize - 4 + height: width + radius: checkRadius + color: hifi.colors.checkboxCheckedBorder + } + Rectangle { id: check width: checkSize @@ -54,7 +68,7 @@ Original.CheckBox { radius: checkRadius anchors.centerIn: parent color: hifi.colors.checkboxChecked - border.width: 1 + border.width: 2 border.color: hifi.colors.checkboxCheckedBorder visible: checked && !pressed || !checked && pressed } diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 56fa0521ae..4304c407c0 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -11,138 +11,124 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 +import QtGraphicalEffects 1.0 import "../styles-uit" Row { - id: thisNameCard; + id: thisNameCard // Spacing - spacing: 10; + spacing: 10 // Anchors - anchors.top: parent.top; + anchors.top: parent.top anchors { - topMargin: (parent.height - contentHeight)/2; - bottomMargin: (parent.height - contentHeight)/2; - leftMargin: 10; - rightMargin: 10; + topMargin: (parent.height - contentHeight)/2 + bottomMargin: (parent.height - contentHeight)/2 + leftMargin: 10 + rightMargin: 10 } // Properties - property int contentHeight: 50; - property string displayName: ""; - property string userName: ""; - property int displayTextHeight: 18; - property int usernameTextHeight: 12; + property int contentHeight: 50 + property string displayName: "" + property string userName: "" + property int displayTextHeight: 18 + property int usernameTextHeight: 12 Column { - id: avatarImage; + id: avatarImage // Size - height: contentHeight; - width: height; + height: contentHeight + width: height Image { - id: userImage; + id: userImage source: "../../icons/defaultNameCardUser.png" // Anchors - width: parent.width; - height: parent.height; + width: parent.width + height: parent.height } } Column { - id: textContainer; + id: textContainer // Size - width: parent.width - avatarImage.width - parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing; - height: contentHeight; + width: parent.width - avatarImage.width - parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing + height: contentHeight // DisplayName Text FiraSansSemiBold { - id: displayNameText; + id: displayNameText // Properties - text: thisNameCard.displayName; - elide: Text.ElideRight; + text: thisNameCard.displayName + elide: Text.ElideRight // Size - width: parent.width; + width: parent.width // Text Size - size: thisNameCard.displayTextHeight; + size: thisNameCard.displayTextHeight // Text Positioning - verticalAlignment: Text.AlignVCenter; + verticalAlignment: Text.AlignVCenter } // UserName Text FiraSansRegular { - id: userNameText; + id: userNameText // Properties - text: thisNameCard.userName; - elide: Text.ElideRight; - visible: thisNameCard.displayName; + text: thisNameCard.userName + elide: Text.ElideRight + visible: thisNameCard.displayName // Size - width: parent.width; + width: parent.width // Text Size - size: thisNameCard.usernameTextHeight; + size: thisNameCard.usernameTextHeight // Text Positioning - verticalAlignment: Text.AlignVCenter; + verticalAlignment: Text.AlignVCenter } // Spacer Item { - height: 7; - width: parent.width; + height: 4 + width: parent.width } // VU Meter - Rectangle { - id: nameCardVUMeter; - objectName: "AvatarInputs"; - width: parent.width; - height: 4; - // Avatar Audio VU Meter - Item { - id: controls; - width: nameCardVUMeter.width; - - Rectangle { - anchors.fill: parent; - color: nameCardVUMeter.audioClipping ? "red" : "#696969"; - - Item { - id: audioMeter - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: nameCardVUMeter.iconPadding - anchors.right: parent.right - anchors.rightMargin: nameCardVUMeter.iconPadding - height: 8 - Rectangle { - id: blueRect - color: "blue" - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - width: parent.width / 4 - } - Rectangle { - id: greenRect - color: "green" - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: blueRect.right - anchors.right: redRect.left - } - Rectangle { - id: redRect - color: "red" - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - width: parent.width / 5 - } - Rectangle { - z: 100 - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - width: (1.0 - nameCardVUMeter.audioLevel) * parent.width - color: "#dddddd"; - } - } + Rectangle { // CHANGEME to the appropriate type! + id: nameCardVUMeter + objectName: "AvatarInputs" + // Size + width: parent.width + height: 8 + // Style + radius: 4 + // Rectangle for the VU meter base + Rectangle { + id: vuMeterBase + // Anchors + anchors.fill: parent + // Style + color: "#eeeeee" + radius: parent.radius + } + // Rectangle for the VU meter audio level + Rectangle { + id: vuMeterLevel + // Size + width: (nameCardVUMeter.audioLevel) * parent.width + // Style + color: "#E3E3E3" + radius: parent.radius + // Anchors + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.left: parent.left + } + // Gradient for the VU meter audio level + LinearGradient { + anchors.fill: vuMeterLevel + source: vuMeterLevel + start: Qt.point(0, 0) + end: Qt.point(parent.width, 0) + gradient: Gradient { + GradientStop { position: 0.05; color: "#00CFEF" } + GradientStop { position: 0.5; color: "#9450A5" } + GradientStop { position: 0.95; color: "#EA4C5F" } } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 3a7c3d45b7..b62c721ead 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -142,15 +142,9 @@ Item { rowDelegate: Rectangle { // The only way I know to specify a row height. // Size height: rowHeight; - // The rest of this is cargo-culted to restore the default styling - SystemPalette { - id: myPalette; - colorGroup: SystemPalette.Active - } - color: { - var baseColor = styleData.alternate?myPalette.alternateBase:myPalette.base - return styleData.selected?myPalette.highlight:baseColor - } + color: styleData.selected + ? "#afafaf" + : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd } // This Item refers to the contents of each Cell @@ -177,7 +171,6 @@ Item { HifiControls.CheckBox { visible: isCheckBox && !isSeparator; anchors.centerIn: parent; - colorScheme: hifi.colorSchemes.dark; boxSize: 22; onClicked: { var newValue = !model[styleData.role]; diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index b09289c78a..8b940e8178 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -17,6 +17,7 @@ #include "Menu.h" HIFI_QML_DEF(AvatarInputs) +HIFI_QML_DEF(AvatarInputs2) static AvatarInputs* INSTANCE{ nullptr }; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 916556fdd7..9d419e5a0f 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -140,7 +140,7 @@ function populateUserList() { function usernameFromIDReply(id, username, machineFingerprint) { var data; // If the ID we've received is our ID... - if (AvatarList.getAvatar('').sessionUUID === id) { + if (MyAvatar.sessionUUID === id) { // Set the data to contain specific strings. data = ['', username] } else { From 1eea3ed27d5ad7c176d64ff3c33934dc209c6fe3 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 21 Dec 2016 12:45:44 -0800 Subject: [PATCH 20/68] No more semicolons; tons of improvements --- .../resources/icons/defaultNameCardUser.png | Bin 50400 -> 3314 bytes .../resources/qml/controls-uit/Table.qml | 2 +- interface/resources/qml/hifi/NameCard.qml | 4 +- interface/resources/qml/hifi/Pal.qml | 290 ++++++++++-------- .../qml/styles-uit/HifiConstants.qml | 4 +- interface/src/ui/AvatarInputs.cpp | 1 - 6 files changed, 164 insertions(+), 137 deletions(-) diff --git a/interface/resources/icons/defaultNameCardUser.png b/interface/resources/icons/defaultNameCardUser.png index 807ca2fa87840b94778e027321d68cfd0288108f..d0c7933db2b26395ac8beffa8377b75b73d74041 100644 GIT binary patch delta 3299 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≫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 diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index 865e24945e..8e685dc253 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -52,7 +52,7 @@ TableView { size: hifi.fontSizes.tableHeadingIcon anchors { left: titleText.right - leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 3 : 0) right: parent.right rightMargin: hifi.dimensions.tablePadding verticalCenter: titleText.verticalCenter diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 4304c407c0..0bc6afd241 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -103,7 +103,7 @@ Row { // Anchors anchors.fill: parent // Style - color: "#eeeeee" + color: "#dbdbdb" // Very appropriate hex value here radius: parent.radius } // Rectangle for the VU meter audio level @@ -112,7 +112,7 @@ Row { // Size width: (nameCardVUMeter.audioLevel) * parent.width // Style - color: "#E3E3E3" + color: "#dbdbdb" // Very appropriate hex value here radius: parent.radius // Anchors anchors.bottom: parent.bottom diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index b62c721ead..35d71b6c42 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -32,116 +32,144 @@ import "../styles-uit" import "../controls-uit" as HifiControls Item { - id: pal; + id: pal // Size - width: parent.width; - height: parent.height; + width: parent.width + height: parent.height // Properties - property int myCardHeight: 70; - property int rowHeight: 70; - property int separatorColWidth: 10; - property int actionButtonWidth: 70; - property int nameCardWidth: width - (iAmAdmin ? separatorColWidth : 0) - actionButtonWidth*(iAmAdmin ? 4 : 2); + property int myCardHeight: 70 + property int rowHeight: 70 + property int separatorColWidth: 10 + property int actionButtonWidth: 75 + property int nameCardWidth: width - (iAmAdmin ? separatorColWidth : 0) - actionButtonWidth*(iAmAdmin ? 4 : 2) // This contains the current user's NameCard and will contain other information in the future Rectangle { - id: myInfo; + id: myInfo // Size - width: pal.width; - height: myCardHeight; + width: pal.width + height: myCardHeight // Anchors - anchors.top: pal.top; + anchors.top: pal.top // Properties - radius: hifi.dimensions.borderRadius; + radius: hifi.dimensions.borderRadius // This NameCard refers to the current user's NameCard (the one above the table) NameCard { - id: myCard; + id: myCard // Properties - displayName: myData.displayName; - userName: myData.userName; + displayName: myData.displayName + userName: myData.userName // Size - width: nameCardWidth; - height: parent.height; + width: nameCardWidth + height: parent.height // Anchors - anchors.left: parent.left; + anchors.left: parent.left } } // Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle Rectangle { - color: "#FFFFFF"; - width: pal.width; - height: 10; - anchors.top: myInfo.bottom; - anchors.left: parent.left; + color: "#FFFFFF" + width: pal.width + height: 10 + anchors.top: myInfo.bottom + anchors.left: parent.left } Rectangle { - color: "#FFFFFF"; - width: pal.width; - height: 10; - anchors.bottom: table.top; - anchors.left: parent.left; + color: "#FFFFFF" + width: pal.width + height: 10 + anchors.bottom: table.top + anchors.left: parent.left + } + // Rectangle that houses "Global Actions" string + Rectangle { + visible: iAmAdmin + color: hifi.colors.tableRowLightEven + width: actionButtonWidth * 2 + separatorColWidth - 2 + height: 40 + anchors.bottom: myInfo.bottom + anchors.bottomMargin: -10 + anchors.right: myInfo.right + radius: hifi.dimensions.borderRadius + RalewaySemiBold { + text: "ADMIN" + size: hifi.fontSizes.tableHeading + 2 + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + anchors.top: parent.top + anchors.topMargin: 8 + anchors.left: parent.left + anchors.right: parent.right + } } // This TableView refers to the table (below the current user's NameCard) HifiControls.Table { - id: table; + id: table // Size - height: pal.height - myInfo.height - 4; - width: pal.width - 4; + height: pal.height - myInfo.height - 4 + width: pal.width - 4 // Anchors - anchors.left: parent.left; - anchors.top: myInfo.bottom; + anchors.left: parent.left + anchors.top: myInfo.bottom // Properties - centerHeaderText: true; - sortIndicatorVisible: true; - headerVisible: true; - onSortIndicatorColumnChanged: sortModel(); - onSortIndicatorOrderChanged: sortModel(); + centerHeaderText: true + sortIndicatorVisible: true + headerVisible: true + onSortIndicatorColumnChanged: sortModel() + onSortIndicatorOrderChanged: sortModel() TableViewColumn { - role: "displayName"; - title: "NAMES"; - width: nameCardWidth; - movable: false; + role: "displayName" + title: "NAMES" + width: nameCardWidth + movable: false + resizable: false } TableViewColumn { - role: "personalMute"; + role: "personalMute" title: "MUTE" - width: actionButtonWidth; - movable: false; + width: actionButtonWidth + movable: false + resizable: false } TableViewColumn { - role: "ignore"; - title: "IGNORE"; - width: actionButtonWidth; - movable: false; + role: "ignore" + title: "IGNORE" + width: actionButtonWidth + movable: false + resizable: false } TableViewColumn { - visible: iAmAdmin; - title: ""; - width: separatorColWidth; - resizable: false; - movable: false; + visible: iAmAdmin + title: "" + width: separatorColWidth + resizable: false + movable: false } TableViewColumn { - visible: iAmAdmin; - role: "mute"; - title: "SILENCE"; - width: actionButtonWidth; - movable: false; + visible: iAmAdmin + role: "mute" + title: "SILENCE" + width: actionButtonWidth + movable: false + resizable: false } TableViewColumn { - visible: iAmAdmin; - role: "kick"; + visible: iAmAdmin + role: "kick" title: "BAN" - width: actionButtonWidth; - movable: false; + width: actionButtonWidth + movable: false + resizable: false } - model: userModel; + model: userModel // This Rectangle refers to each Row in the table. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: rowHeight; + height: rowHeight color: styleData.selected ? "#afafaf" : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd @@ -149,108 +177,108 @@ Item { // This Item refers to the contents of each Cell itemDelegate: Item { - id: itemCell; - property bool isCheckBox: typeof(styleData.value) === 'boolean'; - property bool isSeparator: styleData.value === ''; + id: itemCell + property bool isCheckBox: typeof(styleData.value) === 'boolean' + property bool isSeparator: styleData.value === '' // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { - id: nameCard; + id: nameCard // Properties - displayName: styleData.value; - userName: model.userName; - visible: !isCheckBox && !isSeparator; + displayName: styleData.value + userName: model.userName + visible: !isCheckBox && !isSeparator // Size - width: nameCardWidth; - height: parent.height; + width: nameCardWidth + height: parent.height // Anchors - anchors.left: parent.left; + anchors.left: parent.left } // This CheckBox belongs in the columns that contain the action buttons ("Mute", "Ban", etc) HifiControls.CheckBox { - visible: isCheckBox && !isSeparator; - anchors.centerIn: parent; - boxSize: 22; + visible: isCheckBox && !isSeparator + anchors.centerIn: parent + boxSize: 24 onClicked: { - var newValue = !model[styleData.role]; - var datum = userData[model.userIndex]; - datum[styleData.role] = model[styleData.role] = newValue; - Users[styleData.role](model.sessionId); + var newValue = !model[styleData.role] + var datum = userData[model.userIndex] + datum[styleData.role] = model[styleData.role] = newValue + Users[styleData.role](model.sessionId) // Just for now, while we cannot undo things: - userData.splice(model.userIndex, 1); - sortModel(); + userData.splice(model.userIndex, 1) + sortModel() } } } } // This Rectangle refers to the [?] popup button Rectangle { - color: hifi.colors.tableBackgroundLight; - width: 18; - height: hifi.dimensions.tableHeaderHeight - 2; - anchors.left: table.left; - anchors.top: table.top; - anchors.topMargin: 1; - anchors.leftMargin: nameCardWidth/2 + 23; + color: hifi.colors.tableBackgroundLight + width: 20 + height: hifi.dimensions.tableHeaderHeight - 2 + anchors.left: table.left + anchors.top: table.top + anchors.topMargin: 1 + anchors.leftMargin: nameCardWidth/2 + 24 RalewayRegular { - id: helpText; - text: "[?]"; - size: hifi.fontSizes.tableHeading; - font.capitalization: Font.AllUppercase; - color: hifi.colors.darkGray; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - anchors.fill: parent; + id: helpText + text: "[?]" + size: hifi.fontSizes.tableHeading + 2 + font.capitalization: Font.AllUppercase + color: hifi.colors.darkGray + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent } MouseArea { - anchors.fill: parent; - acceptedButtons: Qt.LeftButton; - hoverEnabled: true; - onClicked: namesPopup.visible = true; - onEntered: helpText.color = hifi.colors.baseGrayHighlight; - onExited: helpText.color = hifi.colors.darkGray; + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: namesPopup.visible = true + onEntered: helpText.color = hifi.colors.baseGrayHighlight + onExited: helpText.color = hifi.colors.darkGray } } // Explanitory popup upon clicking "[?]" Item { - visible: false; - id: namesPopup; - anchors.fill: pal; + visible: false + id: namesPopup + anchors.fill: pal Rectangle { - anchors.fill: parent; - color: "black"; - opacity: 0.5; - radius: hifi.dimensions.borderRadius; + anchors.fill: parent + color: "black" + opacity: 0.5 + radius: hifi.dimensions.borderRadius } Rectangle { - width: Math.min(parent.width * 0.75, 400); - height: popupText.contentHeight*2; - anchors.centerIn: parent; - radius: hifi.dimensions.borderRadius; - color: "white"; + width: Math.min(parent.width * 0.75, 400) + height: popupText.contentHeight*2 + anchors.centerIn: parent + radius: hifi.dimensions.borderRadius + color: "white" FiraSansSemiBold { - id: popupText; - text: "This is temporary text. It will eventually be used to explain what 'Names' means."; - size: hifi.fontSizes.textFieldInput; - color: hifi.colors.darkGray; - horizontalAlignment: Text.AlignHCenter; - anchors.fill: parent; - wrapMode: Text.WordWrap; + id: popupText + text: "This is temporary text. It will eventually be used to explain what 'Names' means." + size: hifi.fontSizes.textFieldInput + color: hifi.colors.darkGray + horizontalAlignment: Text.AlignHCenter + anchors.fill: parent + wrapMode: Text.WordWrap } } MouseArea { - anchors.fill: parent; - acceptedButtons: Qt.LeftButton; + anchors.fill: parent + acceptedButtons: Qt.LeftButton onClicked: { - namesPopup.visible = false; + namesPopup.visible = false } } } - property var userData: []; - property var myData: ({displayName: "", userName: ""}); // valid dummy until set - property bool iAmAdmin: false; + property var userData: [] + property var myData: ({displayName: "", userName: ""}) // valid dummy until set + property bool iAmAdmin: false function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml var i, data = optionalData || userData, length = data.length; for (var i = 0; i < length; i++) { diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index f2698da574..da1b2868a7 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -89,8 +89,8 @@ Item { readonly property color transparent: "#00ffffff" // Control specific colors - readonly property color tableRowLightOdd: "#eaeaea" // Equivalent to white50 over #e3e3e3 background - readonly property color tableRowLightEven: "#c6c6c6" // Equivavlent to "#1a575757" over #e3e3e3 background + readonly property color tableRowLightOdd: "#fafafa" + readonly property color tableRowLightEven: "#eeeeee" // Equivavlent to "#1a575757" over #e3e3e3 background readonly property color tableRowDarkOdd: "#2e2e2e" // Equivalent to "#80393939" over #404040 background readonly property color tableRowDarkEven: "#1c1c1c" // Equivalent to "#a6181818" over #404040 background readonly property color tableBackgroundLight: tableRowLightEven diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 8b940e8178..b09289c78a 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -17,7 +17,6 @@ #include "Menu.h" HIFI_QML_DEF(AvatarInputs) -HIFI_QML_DEF(AvatarInputs2) static AvatarInputs* INSTANCE{ nullptr }; From cf3a260646c64f3a05e88d6e550597afe705dfe8 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 21 Dec 2016 13:13:59 -0800 Subject: [PATCH 21/68] added ability to rotate entity and sound direction --- .../tutorials/entity_scripts/ambientSound.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/tutorials/entity_scripts/ambientSound.js b/scripts/tutorials/entity_scripts/ambientSound.js index 6a892ed139..f3eb93c76f 100644 --- a/scripts/tutorials/entity_scripts/ambientSound.js +++ b/scripts/tutorials/entity_scripts/ambientSound.js @@ -8,6 +8,9 @@ // userData.range should be an integer for the max distance away from the entity where the sound will be audible. // userData.volume is the max volume at which the clip should play. Defaults to 1.0 full volume) // +// The rotation of the entity is copied to the ambisonic field, so by rotating the entity you will rotate the +// direction in-which a certain sound comes from. +// // Remember that the entity has to be visible to the user for the sound to play at all, so make sure the entity is // large enough to be loaded at the range you set, particularly for large ranges. // @@ -27,6 +30,7 @@ var range = DEFAULT_RANGE; var maxVolume = DEFAULT_VOLUME; var UPDATE_INTERVAL_MSECS = 100; + var rotation; var entity; var ambientSound; @@ -92,23 +96,28 @@ this.maybeUpdate = function() { // Every UPDATE_INTERVAL_MSECS, update the volume of the ambient sound based on distance from my avatar _this.updateSettings(); - var props = Entities.getEntityProperties(entity); var HYSTERESIS_FRACTION = 0.1; - var props = Entities.getEntityProperties(entity, [ "position" ]); + var props = Entities.getEntityProperties(entity, [ "position", "rotation" ]); center = props.position; + rotation = props.rotation; var distance = Vec3.length(Vec3.subtract(MyAvatar.position, center)); if (distance <= range) { var volume = (1.0 - distance / range) * maxVolume; if (!soundPlaying && ambientSound.downloaded) { - soundPlaying = Audio.playSound(ambientSound, { loop: true, localOnly: true, volume: volume }); + soundPlaying = Audio.playSound(ambientSound, { loop: true, + localOnly: true, + rotation: rotation, + volume: volume }); debugPrint("Starting ambient sound, volume: " + volume); if (WANT_COLOR_CHANGE) { Entities.editEntity(entity, { color: COLOR_ON }); } } else if (soundPlaying && soundPlaying.playing) { - soundPlaying.setOptions( { volume: volume } ); + debugPrint("Setting volume and rotation: " + Quat.safeEulerAngles(rotation).y); + soundPlaying.setOptions( { volume: volume, orientation: rotation } ); } + } else if (soundPlaying && soundPlaying.playing && (distance > range * HYSTERESIS_FRACTION)) { soundPlaying.stop(); soundPlaying = false; From 632bf997f39b42c527e5ef09a666a7311b5b0da2 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 21 Dec 2016 13:15:44 -0800 Subject: [PATCH 22/68] debug off --- scripts/tutorials/entity_scripts/ambientSound.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/tutorials/entity_scripts/ambientSound.js b/scripts/tutorials/entity_scripts/ambientSound.js index f3eb93c76f..a8e7db239c 100644 --- a/scripts/tutorials/entity_scripts/ambientSound.js +++ b/scripts/tutorials/entity_scripts/ambientSound.js @@ -39,11 +39,11 @@ var checkTimer = false; var _this; - var WANT_COLOR_CHANGE = true; + var WANT_COLOR_CHANGE = false; var COLOR_OFF = { red: 128, green: 128, blue: 128 }; var COLOR_ON = { red: 255, green: 0, blue: 0 }; - var WANT_DEBUG = true; + var WANT_DEBUG = false; function debugPrint(string) { if (WANT_DEBUG) { print(string); @@ -114,7 +114,6 @@ } } else if (soundPlaying && soundPlaying.playing) { - debugPrint("Setting volume and rotation: " + Quat.safeEulerAngles(rotation).y); soundPlaying.setOptions( { volume: volume, orientation: rotation } ); } From b02d14fcc2f899fbb8ce5036b85b22f2affed5ae Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Wed, 21 Dec 2016 13:17:08 -0800 Subject: [PATCH 23/68] set right thing initially --- scripts/tutorials/entity_scripts/ambientSound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tutorials/entity_scripts/ambientSound.js b/scripts/tutorials/entity_scripts/ambientSound.js index a8e7db239c..620b371400 100644 --- a/scripts/tutorials/entity_scripts/ambientSound.js +++ b/scripts/tutorials/entity_scripts/ambientSound.js @@ -106,7 +106,7 @@ if (!soundPlaying && ambientSound.downloaded) { soundPlaying = Audio.playSound(ambientSound, { loop: true, localOnly: true, - rotation: rotation, + orientation: rotation, volume: volume }); debugPrint("Starting ambient sound, volume: " + volume); if (WANT_COLOR_CHANGE) { From 7c51d47ae437b711798a85bf89a8489fd08ef228 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 21 Dec 2016 14:11:14 -0800 Subject: [PATCH 24/68] Better separator --- interface/resources/qml/hifi/Pal.qml | 57 ++++++++++++++++++---------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 35d71b6c42..af99d9a2e1 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -39,9 +39,8 @@ Item { // Properties property int myCardHeight: 70 property int rowHeight: 70 - property int separatorColWidth: 10 property int actionButtonWidth: 75 - property int nameCardWidth: width - (iAmAdmin ? separatorColWidth : 0) - actionButtonWidth*(iAmAdmin ? 4 : 2) + property int nameCardWidth: width - actionButtonWidth*(iAmAdmin ? 4 : 2) // This contains the current user's NameCard and will contain other information in the future Rectangle { @@ -81,27 +80,39 @@ Item { anchors.bottom: table.top anchors.left: parent.left } - // Rectangle that houses "Global Actions" string + // Rectangle that houses "ADMIN" string Rectangle { - visible: iAmAdmin - color: hifi.colors.tableRowLightEven - width: actionButtonWidth * 2 + separatorColWidth - 2 + id: adminTab + // Size + width: actionButtonWidth * 2 - 2 height: 40 + // Anchors anchors.bottom: myInfo.bottom anchors.bottomMargin: -10 anchors.right: myInfo.right + // Properties + visible: iAmAdmin + // Style + color: hifi.colors.tableRowLightEven radius: hifi.dimensions.borderRadius + border.color: hifi.colors.lightGrayText + border.width: 2 + // "ADMIN" text RalewaySemiBold { text: "ADMIN" + // Text size size: hifi.fontSizes.tableHeading + 2 - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignTop + // Anchors anchors.top: parent.top anchors.topMargin: 8 anchors.left: parent.left anchors.right: parent.right + // Style + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop } } // This TableView refers to the table (below the current user's NameCard) @@ -141,13 +152,6 @@ Item { movable: false resizable: false } - TableViewColumn { - visible: iAmAdmin - title: "" - width: separatorColWidth - resizable: false - movable: false - } TableViewColumn { visible: iAmAdmin role: "mute" @@ -179,7 +183,6 @@ Item { itemDelegate: Item { id: itemCell property bool isCheckBox: typeof(styleData.value) === 'boolean' - property bool isSeparator: styleData.value === '' // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { @@ -187,7 +190,7 @@ Item { // Properties displayName: styleData.value userName: model.userName - visible: !isCheckBox && !isSeparator + visible: !isCheckBox // Size width: nameCardWidth height: parent.height @@ -197,7 +200,7 @@ Item { // This CheckBox belongs in the columns that contain the action buttons ("Mute", "Ban", etc) HifiControls.CheckBox { - visible: isCheckBox && !isSeparator + visible: isCheckBox anchors.centerIn: parent boxSize: 24 onClicked: { @@ -212,6 +215,18 @@ Item { } } } + // Separator between user and admin functions + Rectangle { + // Size + width: 2 + height: table.height + // Anchors + anchors.left: adminTab.left + anchors.top: table.top + // Properties + visible: iAmAdmin + color: hifi.colors.lightGrayText + } // This Rectangle refers to the [?] popup button Rectangle { color: hifi.colors.tableBackgroundLight @@ -356,7 +371,7 @@ Item { datum[property] = false; } } - ['personalMute', 'ignore', 'spacer', 'mute', 'kick'].forEach(init); + ['personalMute', 'ignore', 'mute', 'kick'].forEach(init); datum.userIndex = userIndex++; userModel.append(datum); }); From 890e35e96ec38e1aa24c892ac4b6a9efdf2bc064 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 21 Dec 2016 15:37:23 -0800 Subject: [PATCH 25/68] cleanup unnecessary scope and swap if-check order --- libraries/animation/src/Rig.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b6bb8ccc8f..eec8035957 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1195,7 +1195,7 @@ bool Rig::getModelRegistrationPoint(glm::vec3& modelRegistrationPointOut) const void Rig::applyOverridePoses() { PerformanceTimer perfTimer("override"); - if (!_animSkeleton || _numOverrides == 0) { + if (_numOverrides == 0 || !_animSkeleton) { return; } @@ -1218,17 +1218,15 @@ void Rig::buildAbsoluteRigPoses(const AnimPoseVec& relativePoses, AnimPoseVec& a ASSERT(_animSkeleton->getNumJoints() == (int)relativePoses.size()); - { - absolutePosesOut.resize(relativePoses.size()); - AnimPose geometryToRigTransform(_geometryToRigTransform); - for (int i = 0; i < (int)relativePoses.size(); i++) { - int parentIndex = _animSkeleton->getParentIndex(i); - if (parentIndex == -1) { - // transform all root absolute poses into rig space - absolutePosesOut[i] = geometryToRigTransform * relativePoses[i]; - } else { - absolutePosesOut[i] = absolutePosesOut[parentIndex] * relativePoses[i]; - } + absolutePosesOut.resize(relativePoses.size()); + AnimPose geometryToRigTransform(_geometryToRigTransform); + for (int i = 0; i < (int)relativePoses.size(); i++) { + int parentIndex = _animSkeleton->getParentIndex(i); + if (parentIndex == -1) { + // transform all root absolute poses into rig space + absolutePosesOut[i] = geometryToRigTransform * relativePoses[i]; + } else { + absolutePosesOut[i] = absolutePosesOut[parentIndex] * relativePoses[i]; } } } From d01500a7bbafcaa8cf608af45f44ed44005abc7e Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 21 Dec 2016 17:53:30 -0800 Subject: [PATCH 26/68] Fix the bug? --- interface/resources/qml/hifi/dialogs/RunningScripts.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 3f05a140ae..29807d9646 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -22,7 +22,7 @@ ScrollingWindow { objectName: "RunningScripts" title: "Running Scripts" resizable: true - destroyOnHidden: true + destroyOnHidden: false implicitWidth: 424 implicitHeight: isHMD ? 695 : 728 minSize: Qt.vector2d(424, 300) From b3be7f0f3eedea240de5fd05401ce0dcc2c62977 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 22 Dec 2016 10:42:05 -0800 Subject: [PATCH 27/68] restore expensive version of Avatar::updatePalms() --- interface/src/avatar/Avatar.cpp | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index a79d2372c5..dc5b6233aa 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1293,23 +1293,10 @@ void Avatar::setOrientation(const glm::quat& orientation) { void Avatar::updatePalms() { PerformanceTimer perfTimer("palms"); // update thread-safe caches - glm::quat rotation; - int i = _skeletonModel->getLeftHandJointIndex(); - if (_skeletonModel->getJointRotationInWorldFrame(i, rotation)) { - _leftPalmRotationCache.set(rotation); - } - glm::vec3 position; - if (_skeletonModel->getJointPositionInWorldFrame(i, position)) { - _leftPalmPositionCache.set(position); - } - - i = _skeletonModel->getRightHandJointIndex(); - if (_skeletonModel->getJointRotationInWorldFrame(i, rotation)) { - _rightPalmRotationCache.set(rotation); - } - if (_skeletonModel->getJointPositionInWorldFrame(i, position)) { - _rightPalmPositionCache.set(position); - } + _leftPalmRotationCache.set(getUncachedLeftPalmRotation()); + _rightPalmRotationCache.set(getUncachedRightPalmRotation()); + _leftPalmPositionCache.set(getUncachedLeftPalmPosition()); + _rightPalmPositionCache.set(getUncachedRightPalmPosition()); } void Avatar::setParentID(const QUuid& parentID) { From 4161775673c5709cbd76d7ee133d284b9f2c60e8 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 22 Dec 2016 10:54:58 -0800 Subject: [PATCH 28/68] restore context around lock --- libraries/animation/src/Rig.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index eec8035957..6066905943 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -929,8 +929,10 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); // copy internal poses to external poses - QWriteLocker writeLock(&_externalPoseSetLock); - _externalPoseSet = _internalPoseSet; + { + QWriteLocker writeLock(&_externalPoseSetLock); + _externalPoseSet = _internalPoseSet; + } } void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, From e58623bcc0efccb1770c8e5d614722be46fedabe Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 22 Dec 2016 11:46:40 -0800 Subject: [PATCH 29/68] Working on test/trace script interface --- .../src/scripting/TestScriptingInterface.cpp | 78 ++++++++++++++++++- .../src/scripting/TestScriptingInterface.h | 20 ++++- libraries/networking/src/ResourceCache.cpp | 9 +++ libraries/networking/src/ResourceCache.h | 3 + .../oculus/src/OculusBaseDisplayPlugin.cpp | 1 - plugins/oculus/src/OculusDisplayPlugin.cpp | 1 - tests/render-perf/src/main.cpp | 3 + 7 files changed, 109 insertions(+), 6 deletions(-) diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index 1e1a4d8d0c..8bc601ec83 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -5,15 +5,17 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - - #include "TestScriptingInterface.h" #include #include +#include #include #include +#include + +#include "Application.h" TestScriptingInterface* TestScriptingInterface::getInstance() { static TestScriptingInterface sharedInstance; @@ -25,12 +27,51 @@ void TestScriptingInterface::quit() { } void TestScriptingInterface::waitForTextureIdle() { + waitForCondition(0, []()->bool { + return (0 == gpu::Context::getTextureGPUTransferCount()); + }); } void TestScriptingInterface::waitForDownloadIdle() { + waitForCondition(0, []()->bool { + return (0 == ResourceCache::getLoadingRequestCount()) && (0 == ResourceCache::getPendingRequestCount()); + }); +} + +void TestScriptingInterface::waitForProcessingIdle() { + auto statTracker = DependencyManager::get(); + waitForCondition(0, [statTracker]()->bool { + return (0 == statTracker->getStat("Processing").toInt() && 0 == statTracker->getStat("PendingProcessing").toInt()); + }); } void TestScriptingInterface::waitIdle() { + // Initial wait for some incoming work + QThread::sleep(1); + waitForDownloadIdle(); + waitForProcessingIdle(); + waitForTextureIdle(); +} + +bool TestScriptingInterface::loadTestScene(QString scene) { + // FIXME implement + // qApp->postLambdaEvent([isClient] { + // auto tree = qApp->getEntityClipboard(); + // tree->setIsClient(isClient); + // }); + /* + Test.setClientTree(false); + Resources.overrideUrlPrefix("atp:/", TEST_BINARY_ROOT + scene + ".atp/"); + if (!Clipboard.importEntities(TEST_SCENES_ROOT + scene + ".json")) { + return false; + } + var position = { x: 0, y: 0, z: 0 }; + var pastedEntityIDs = Clipboard.pasteEntities(position); + for (var id in pastedEntityIDs) { + print("QQQ ID imported " + id); + } + */ + return false; } bool TestScriptingInterface::startTracing(QString logrules) { @@ -55,4 +96,35 @@ bool TestScriptingInterface::stopTracing(QString filename) { tracer->stopTracing(); tracer->serialize(filename); return true; -} \ No newline at end of file +} + +void TestScriptingInterface::clear() { + qApp->postLambdaEvent([] { + qApp->getEntities()->clear(); + }); +} + +bool TestScriptingInterface::waitForConnection(qint64 maxWaitMs) { + // Wait for any previous connection to die + QThread::sleep(1); + return waitForCondition(maxWaitMs, []()->bool { + return DependencyManager::get()->getDomainHandler().isConnected(); + }); +} + +void TestScriptingInterface::wait(int milliseconds) { + QThread::msleep(milliseconds); +} + +bool TestScriptingInterface::waitForCondition(qint64 maxWaitMs, std::function condition) { + QElapsedTimer elapsed; + elapsed.start(); + while (!condition()) { + if (maxWaitMs > 0 && elapsed.elapsed() > maxWaitMs) { + return false; + } + QThread::msleep(1); + } + return condition(); +} + diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 0c6ab6baaa..6307289ac2 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -10,6 +10,7 @@ #ifndef hifi_TestScriptingInterface_h #define hifi_TestScriptingInterface_h +#include #include class TestScriptingInterface : public QObject { @@ -34,10 +35,24 @@ public slots: void waitForDownloadIdle(); /**jsdoc - * Waits for all pending downloads and texture transfers to be complete + * Waits for all file parsing operations to be complete + */ + void waitForProcessingIdle(); + + /**jsdoc + * Waits for all pending downloads, parsing and texture transfers to be complete */ void waitIdle(); + + bool waitForConnection(qint64 maxWaitMs = 10000); + + void wait(int milliseconds); + + bool loadTestScene(QString sceneFile); + + void clear(); + /**jsdoc * Start recording Chrome compatible tracing events * logRules can be used to specify a set of logging category rules to limit what gets captured @@ -49,6 +64,9 @@ public slots: * Using a filename with a .gz extension will automatically compress the output file */ bool stopTracing(QString filename); + +private: + bool waitForCondition(qint64 maxWaitMs, std::function condition); }; #endif // hifi_TestScriptingInterface_h diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 677be8ecf5..d95c6f140f 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -71,6 +71,11 @@ QList> ResourceCacheSharedItems::getLoadingRequests() { return result; } +uint32_t ResourceCacheSharedItems::getLoadingRequestsCount() const { + Lock lock(_mutex); + return _loadingRequests.size(); +} + void ResourceCacheSharedItems::removeRequest(QWeakPointer resource) { Lock lock(_mutex); @@ -463,6 +468,10 @@ int ResourceCache::getPendingRequestCount() { return DependencyManager::get()->getPendingRequestsCount(); } +int ResourceCache::getLoadingRequestCount() { + return DependencyManager::get()->getLoadingRequestsCount(); +} + bool ResourceCache::attemptRequest(QSharedPointer resource) { Q_ASSERT(!resource.isNull()); auto sharedItems = DependencyManager::get(); diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 4231785616..03d37bc9ac 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -73,6 +73,7 @@ public: uint32_t getPendingRequestsCount() const; QList> getLoadingRequests(); QSharedPointer getHighestPendingRequest(); + uint32_t getLoadingRequestsCount() const; private: ResourceCacheSharedItems() = default; @@ -241,6 +242,8 @@ public: static int getPendingRequestCount(); + static int getLoadingRequestCount(); + ResourceCache(QObject* parent = nullptr); virtual ~ResourceCache(); diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 5db5840f43..26906ef2fb 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -137,5 +137,4 @@ void OculusBaseDisplayPlugin::updatePresentPose() { } OculusBaseDisplayPlugin::~OculusBaseDisplayPlugin() { - qDebug() << "Destroying OculusBaseDisplayPlugin"; } diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 2e9f74a0e5..88ca02edc2 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -183,5 +183,4 @@ QString OculusDisplayPlugin::getPreferredAudioOutDevice() const { } OculusDisplayPlugin::~OculusDisplayPlugin() { - qDebug() << "Destroying OculusDisplayPlugin"; } diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index bd731564e7..31bb13f4d0 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -475,6 +476,8 @@ public: DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(NodeType::Agent); DependencyManager::set(); From be6b098e1b376c7d406dbec667a9ad572b9301c9 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 22 Dec 2016 13:12:59 -0800 Subject: [PATCH 30/68] Implement test scene loading --- .../src/scripting/TestScriptingInterface.cpp | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index 8bc601ec83..3d9e84ce63 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "Application.h" @@ -54,24 +55,20 @@ void TestScriptingInterface::waitIdle() { } bool TestScriptingInterface::loadTestScene(QString scene) { - // FIXME implement - // qApp->postLambdaEvent([isClient] { - // auto tree = qApp->getEntityClipboard(); - // tree->setIsClient(isClient); - // }); - /* - Test.setClientTree(false); - Resources.overrideUrlPrefix("atp:/", TEST_BINARY_ROOT + scene + ".atp/"); - if (!Clipboard.importEntities(TEST_SCENES_ROOT + scene + ".json")) { - return false; - } - var position = { x: 0, y: 0, z: 0 }; - var pastedEntityIDs = Clipboard.pasteEntities(position); - for (var id in pastedEntityIDs) { - print("QQQ ID imported " + id); - } - */ - return false; + static const QString TEST_ROOT = "https://raw.githubusercontent.com/highfidelity/hifi_tests/master/"; + static const QString TEST_BINARY_ROOT = "https://hifi-public.s3.amazonaws.com/test_scene_data/"; + static const QString TEST_SCRIPTS_ROOT = TEST_ROOT + "scripts/"; + static const QString TEST_SCENES_ROOT = TEST_ROOT + "scenes/"; + return DependencyManager::get()->returnFromUiThread([scene]()->QVariant { + ResourceManager::setUrlPrefixOverride("atp:/", TEST_BINARY_ROOT + scene + ".atp/"); + auto tree = qApp->getEntities()->getTree(); + auto treeIsClient = tree->getIsClient(); + // Force the tree to accept the load regardless of permissions + tree->setIsClient(false); + auto result = tree->readFromURL(TEST_SCENES_ROOT + scene + ".json"); + tree->setIsClient(treeIsClient); + return result; + }).toBool(); } bool TestScriptingInterface::startTracing(QString logrules) { From 2e897e0cc9fc0dcd1788c0303d1b688688af6e74 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 22 Dec 2016 14:01:03 -0800 Subject: [PATCH 31/68] Working first pass Seems roughly same as mic meter. Works for other avatars, though using agent avatars (crowd-agent.js + summon.js), seems not to work. I'll investigate that... --- interface/resources/qml/hifi/NameCard.qml | 4 +- interface/resources/qml/hifi/Pal.qml | 16 +++++++- scripts/system/pal.js | 46 ++++++++++++++++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 0bc6afd241..9b90ae6c3b 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -33,6 +33,7 @@ Row { property string userName: "" property int displayTextHeight: 18 property int usernameTextHeight: 12 + property real audioLevel: 0.0 Column { id: avatarImage @@ -91,7 +92,6 @@ Row { // VU Meter Rectangle { // CHANGEME to the appropriate type! id: nameCardVUMeter - objectName: "AvatarInputs" // Size width: parent.width height: 8 @@ -110,7 +110,7 @@ Row { Rectangle { id: vuMeterLevel // Size - width: (nameCardVUMeter.audioLevel) * parent.width + width: (thisNameCard.audioLevel) * parent.width // Style color: "#dbdbdb" // Very appropriate hex value here radius: parent.radius diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index af99d9a2e1..aac3cc2e2c 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -58,6 +58,7 @@ Item { // Properties displayName: myData.displayName userName: myData.userName + audioLevel: myData.audioLevel // Size width: nameCardWidth height: parent.height @@ -190,6 +191,7 @@ Item { // Properties displayName: styleData.value userName: model.userName + audioLevel: model.audioLevel visible: !isCheckBox // Size width: nameCardWidth @@ -292,7 +294,7 @@ Item { } property var userData: [] - property var myData: ({displayName: "", userName: ""}) // valid dummy until set + property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set property bool iAmAdmin: false function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml var i, data = optionalData || userData, length = data.length; @@ -343,6 +345,18 @@ Item { userData[userIndex].userName = userName; // Defensive programming } break; + case 'updateAudioLevel': + var userId = message.params[0]; + var audioLevel = message.params[1]; + if (!userId) { + myData.audioLevel = audioLevel; + myCard.audioLevel = audioLevel; + } else { + var userIndex = findSessionIndex(userId); + userModel.get(userIndex).audioLevel = audioLevel; + userData[userIndex].audioLevel = audioLevel; + } + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 9d419e5a0f..14b3f483da 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -119,7 +119,8 @@ function populateUserList() { var avatarPalDatum = { displayName: avatar.sessionDisplayName, userName: '', - sessionId: id || '' + sessionId: id || '', + audioLevel: getAudioLevel(id) }; // If the current user is an admin OR // they're requesting their own username ("id" is blank)... @@ -262,6 +263,49 @@ function onClicked() { pal.setVisible(!pal.visible); } +var AVERAGING_RATIO = 0.05 +var LOUDNESS_FLOOR = 11.0; +var LOUDNESS_SCALE = 2.8 / 5.0; +var LOG2 = Math.log(2.0); +var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) +var accumulatedLevels={}; + +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; + + // we will do exponential moving average by taking some the last loudness and averaging + accumulatedLevels[id] = AVERAGING_RATIO*(accumulatedLevels[id] || 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). + var logLevel = Math.log(accumulatedLevels[id]+1) / LOG2; + + if (logLevel <= LOUDNESS_FLOOR) { + audioLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE; + } else { + audioLevel = (logLevel - (LOUDNESS_FLOOR - 1.0)) * LOUDNESS_SCALE; + } + if (audioLevel > 1.0) { + audioLevel = 1; + } + return audioLevel; +} + + +// we will update the audioLevels periodically +// TODO: tune for efficiency - expecially with large numbers of avatars +Script.setInterval(function () { + if (pal.visible) { + AvatarList.getAvatarIdentifiers().sort().forEach(function (id) { + var level = getAudioLevel(id); + pal.sendToQml({method: 'updateAudioLevel', params: [id, level]}); + }); + } +}, AUDIO_LEVEL_UPDATE_INTERVAL_MS); // // Button state. // From a5efc08473ed05209a89a782617e3dde77c4eaf1 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 22 Dec 2016 15:32:30 -0800 Subject: [PATCH 32/68] use PROFILE_COUNTER not SAMPLE_PROFILE_COUNTER --- interface/src/avatar/AvatarManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 53f17e9635..c5222641ff 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -170,9 +170,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { // simulate avatar fades simulateAvatarFades(deltaTime); - SAMPLE_PROFILE_COUNTER(0.1f, simulation_avatar, "NumAvatarsPerSec", + PROFILE_COUNTER(simulation_avatar, "NumAvatarsPerSec", { { "NumAvatarsPerSec", (float)(size() * USECS_PER_SECOND) / (float)(usecTimestampNow() - start) } }); - SAMPLE_PROFILE_COUNTER(0.1f, simulation_avatar, "NumJointsPerSec", { { "NumJointsPerSec", Avatar::getNumJointsProcessedPerSecond() } }); + PROFILE_COUNTER(simulation_avatar, "NumJointsPerSec", { { "NumJointsPerSec", Avatar::getNumJointsProcessedPerSecond() } }); } void AvatarManager::postUpdate(float deltaTime) { From fc4763be038f06885c15628bf1394b8263d3f0f2 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 22 Dec 2016 15:55:17 -0800 Subject: [PATCH 33/68] use PROFILE_COUNTER not SAMPLE_PROFILE_COUNTER --- interface/src/avatar/AvatarManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 53f17e9635..c5222641ff 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -170,9 +170,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { // simulate avatar fades simulateAvatarFades(deltaTime); - SAMPLE_PROFILE_COUNTER(0.1f, simulation_avatar, "NumAvatarsPerSec", + PROFILE_COUNTER(simulation_avatar, "NumAvatarsPerSec", { { "NumAvatarsPerSec", (float)(size() * USECS_PER_SECOND) / (float)(usecTimestampNow() - start) } }); - SAMPLE_PROFILE_COUNTER(0.1f, simulation_avatar, "NumJointsPerSec", { { "NumJointsPerSec", Avatar::getNumJointsProcessedPerSecond() } }); + PROFILE_COUNTER(simulation_avatar, "NumJointsPerSec", { { "NumJointsPerSec", Avatar::getNumJointsProcessedPerSecond() } }); } void AvatarManager::postUpdate(float deltaTime) { From 966cbb768dca3fc3de1cc8f8b1abe4f0fc2d290a Mon Sep 17 00:00:00 2001 From: Christoph Haag Date: Fri, 23 Dec 2016 01:02:58 +0100 Subject: [PATCH 34/68] fix GL 4.5 when glTextureSubImage2DEXT unavailable For example mesa --- libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 893d18f541..9b16908244 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -398,9 +398,13 @@ bool GL45Texture::continueTransfer() { glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); } else if (GL_TEXTURE_CUBE_MAP == _target) { // DSA ARB does not work on AMD, so use EXT - // glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); - auto target = CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else { + glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } } else { Q_ASSERT(false); } From f2bdbe051c140e7af4643d2af11fd4de16fe10dc Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 22 Dec 2016 16:35:12 -0800 Subject: [PATCH 35/68] Halve the volume --- scripts/system/bubble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index f02f69729b..2f7286872e 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -64,7 +64,7 @@ Audio.playSound(bubbleActivateSound, { position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, localOnly: true, - volume: 0.4 + volume: 0.2 }); hideOverlays(); if (updateConnected === true) { From 9eed430f49340c7aab2fad59d3a81a9334c5574e Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 22 Dec 2016 18:09:19 -0800 Subject: [PATCH 36/68] CR feedback --- interface/resources/qml/hifi/Pal.qml | 21 ++++++++++++--------- scripts/system/pal.js | 8 ++++++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index aac3cc2e2c..a03556a56e 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -346,15 +346,18 @@ Item { } break; case 'updateAudioLevel': - var userId = message.params[0]; - var audioLevel = message.params[1]; - if (!userId) { - myData.audioLevel = audioLevel; - myCard.audioLevel = audioLevel; - } else { - var userIndex = findSessionIndex(userId); - userModel.get(userIndex).audioLevel = audioLevel; - userData[userIndex].audioLevel = audioLevel; + for (var userId in message.params) { + var audioLevel = message.params[userId]; + // If the userId is 0, we're updating "myData". + if (userId == 0) { + myData.audioLevel = audioLevel; + myCard.audioLevel = audioLevel; // Defensive programming + } else { + console.log("userid:" + userId); + var userIndex = findSessionIndex(userId); + userModel.get(userIndex).audioLevel = audioLevel; + userData[userIndex].audioLevel = audioLevel; // Defensive programming + } } break; default: diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 14b3f483da..a19c6a8c29 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -120,7 +120,7 @@ function populateUserList() { displayName: avatar.sessionDisplayName, userName: '', sessionId: id || '', - audioLevel: getAudioLevel(id) + audioLevel: 0.0 }; // If the current user is an admin OR // they're requesting their own username ("id" is blank)... @@ -300,10 +300,14 @@ function getAudioLevel(id) { // TODO: tune for efficiency - expecially with large numbers of avatars Script.setInterval(function () { if (pal.visible) { + var param = {}; AvatarList.getAvatarIdentifiers().sort().forEach(function (id) { var level = getAudioLevel(id); - pal.sendToQml({method: 'updateAudioLevel', params: [id, level]}); + // qml didn't like an object with null/empty string for a key, so... + var userId = id || 0; + param[userId]= level; }); + pal.sendToQml({method: 'updateAudioLevel', params: param}); } }, AUDIO_LEVEL_UPDATE_INTERVAL_MS); // From 5490dddbbf6fdb64fb4c2ca5096b23a195ed3865 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 23 Dec 2016 12:22:33 -0800 Subject: [PATCH 37/68] Cool style changes! --- interface/resources/qml/hifi/Pal.qml | 42 +++++++++++++++++++++++++++- scripts/system/pal.js | 10 +++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index af99d9a2e1..69e4b1d3e7 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -47,7 +47,7 @@ Item { id: myInfo // Size width: pal.width - height: myCardHeight + height: myCardHeight + 20 // Anchors anchors.top: pal.top // Properties @@ -215,6 +215,46 @@ Item { } } } + // Refresh button + Rectangle { + // Size + width: hifi.dimensions.tableHeaderHeight-1 + height: hifi.dimensions.tableHeaderHeight-1 + // Anchors + anchors.left: table.left + anchors.leftMargin: 4 + anchors.top: table.top + // Style + color: hifi.colors.tableBackgroundLight + // Actual refresh icon + HiFiGlyphs { + id: reloadButton + text: hifi.glyphs.reloadSmall + // Size + size: parent.width*1.5 + // Anchors + anchors.fill: parent + // Style + horizontalAlignment: Text.AlignHCenter + color: hifi.colors.darkGray + } + MouseArea { + id: reloadButtonArea + // Anchors + anchors.fill: parent + hoverEnabled: true + // Everyone likes a responsive refresh button! + // So use onPressed instead of onClicked + onPressed: { + reloadButton.color = hifi.colors.lightGrayText + pal.sendToScript({method: 'refresh'}) + } + onReleased: reloadButton.color = (containsMouse ? hifi.colors.baseGrayHighlight : hifi.colors.darkGray) + onEntered: reloadButton.color = hifi.colors.baseGrayHighlight + onExited: reloadButton.color = (pressed ? hifi.colors.lightGrayText: hifi.colors.darkGray) + } + } + // Separator between user and admin functions Rectangle { // Size diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 9d419e5a0f..be87069b54 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -94,6 +94,10 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like overlay.select(selected); }); break; + case 'refresh': + removeOverlays(); + populateUserList(); + break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } @@ -265,13 +269,13 @@ function onClicked() { // // Button state. // -function onVisibileChanged() { +function onVisibleChanged() { button.writeProperty('buttonState', pal.visible ? 0 : 1); button.writeProperty('defaultState', pal.visible ? 0 : 1); button.writeProperty('hoverState', pal.visible ? 2 : 3); } button.clicked.connect(onClicked); -pal.visibleChanged.connect(onVisibileChanged); +pal.visibleChanged.connect(onVisibleChanged); pal.closed.connect(off); Users.usernameFromIDReply.connect(usernameFromIDReply); @@ -281,7 +285,7 @@ Users.usernameFromIDReply.connect(usernameFromIDReply); Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); toolBar.removeButton(buttonName); - pal.visibleChanged.disconnect(onVisibileChanged); + pal.visibleChanged.disconnect(onVisibleChanged); pal.closed.disconnect(off); Users.usernameFromIDReply.disconnect(usernameFromIDReply); off(); From 1cb330057e8ba1a88eff78815c4ca597301cdac2 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 23 Dec 2016 13:05:57 -0800 Subject: [PATCH 38/68] fix a crash and a memory leak in vhacd-util --- libraries/shared/src/Trace.h | 5 ++++- tools/vhacd-util/src/VHACDUtil.cpp | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/shared/src/Trace.h b/libraries/shared/src/Trace.h index f719969758..ee4f28f0ce 100644 --- a/libraries/shared/src/Trace.h +++ b/libraries/shared/src/Trace.h @@ -101,7 +101,10 @@ private: }; inline void traceEvent(const QLoggingCategory& category, const QString& name, EventType type, const QString& id = "", const QVariantMap& args = {}, const QVariantMap& extra = {}) { - DependencyManager::get()->traceEvent(category, name, type, id, args, extra); + const auto& tracer = DependencyManager::get(); + if (tracer) { + tracer->traceEvent(category, name, type, id, args, extra); + } } inline void traceEvent(const QLoggingCategory& category, const QString& name, EventType type, int id, const QVariantMap& args = {}, const QVariantMap& extra = {}) { diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 9e28d33120..30d0b5e772 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -51,6 +51,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { return false; } result = *geom; + delete geom; reSortFBXGeometryMeshes(result); } catch (const QString& error) { From 5065f027e79787b6da58385756ac36cfda93e5a1 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 23 Dec 2016 13:11:42 -0800 Subject: [PATCH 39/68] whitespace --- scripts/system/pal.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/system/pal.js b/scripts/system/pal.js index a19c6a8c29..2b24a76cae 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -268,7 +268,7 @@ var LOUDNESS_FLOOR = 11.0; var LOUDNESS_SCALE = 2.8 / 5.0; var LOG2 = Math.log(2.0); var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) -var accumulatedLevels={}; +var accumulatedLevels = {}; function getAudioLevel(id) { // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged @@ -278,11 +278,11 @@ function getAudioLevel(id) { var audioLevel = 0.0; // we will do exponential moving average by taking some the last loudness and averaging - accumulatedLevels[id] = AVERAGING_RATIO*(accumulatedLevels[id] || 0 ) + (1-AVERAGING_RATIO)*(avatar.audioLoudness); + accumulatedLevels[id] = AVERAGING_RATIO * (accumulatedLevels[id] || 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). - var logLevel = Math.log(accumulatedLevels[id]+1) / LOG2; + var logLevel = Math.log(accumulatedLevels[id] + 1) / LOG2; if (logLevel <= LOUDNESS_FLOOR) { audioLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE; From b878051cd94d971463e8b4df794d7605f51ec74f Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 24 Dec 2016 08:42:56 -0800 Subject: [PATCH 40/68] Ambisonic limiter, with gain linking between all channels to preserve imaging --- libraries/audio/src/AudioLimiter.cpp | 164 ++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 5 deletions(-) diff --git a/libraries/audio/src/AudioLimiter.cpp b/libraries/audio/src/AudioLimiter.cpp index 7bbaca62ca..e0808c1daa 100644 --- a/libraries/audio/src/AudioLimiter.cpp +++ b/libraries/audio/src/AudioLimiter.cpp @@ -211,6 +211,49 @@ static inline int32_t peaklog2(float* input0, float* input1) { return (e << LOG2_FRACBITS) - (c2 >> 3); } +// +// Peak detection and -log2(x) for float input (quad) +// x < 2^(31-LOG2_HEADROOM) returns 0x7fffffff +// x > 2^LOG2_HEADROOM undefined +// +static inline int32_t peaklog2(float* input0, float* input1, float* input2, float* input3) { + + // float as integer bits + int32_t u0 = *(int32_t*)input0; + int32_t u1 = *(int32_t*)input1; + int32_t u2 = *(int32_t*)input2; + int32_t u3 = *(int32_t*)input3; + + // max absolute value + u0 &= IEEE754_FABS_MASK; + u1 &= IEEE754_FABS_MASK; + u2 &= IEEE754_FABS_MASK; + u3 &= IEEE754_FABS_MASK; + int32_t peak = MAX(MAX(u0, u1), MAX(u2, u3)); + + // split into e and x - 1.0 + int32_t e = IEEE754_EXPN_BIAS - (peak >> IEEE754_MANT_BITS) + LOG2_HEADROOM; + int32_t x = (peak << (31 - IEEE754_MANT_BITS)) & 0x7fffffff; + + // saturate + if (e > 31) { + return 0x7fffffff; + } + + int k = x >> (31 - LOG2_TABBITS); + + // polynomial for log2(1+x) over x=[0,1] + int32_t c0 = log2Table[k][0]; + int32_t c1 = log2Table[k][1]; + int32_t c2 = log2Table[k][2]; + + c1 += MULHI(c0, x); + c2 += MULHI(c1, x); + + // reconstruct result in Q26 + return (e << LOG2_FRACBITS) - (c2 >> 3); +} + // // Compute exp2(-x) for x=[0,32] in Q26, result in Q31 // x < 0 undefined @@ -376,6 +419,39 @@ public: } }; +// +// N-1 sample delay (quad) +// +template +class QuadDelay { + + static_assert((N & (N - 1)) == 0, "N must be a power of 2"); + + float _buffer[4*N] = {}; + int _index = 0; + +public: + void process(float& x0, float& x1, float& x2, float& x3) { + + const int MASK = 4*N - 1; // buffer wrap + int i = _index; + + _buffer[i+0] = x0; + _buffer[i+1] = x1; + _buffer[i+2] = x2; + _buffer[i+3] = x3; + + i = (i + 4*(N - 1)) & MASK; + + x0 = _buffer[i+0]; + x1 = _buffer[i+1]; + x2 = _buffer[i+2]; + x3 = _buffer[i+3]; + + _index = i; + } +}; + // // Limiter (common) // @@ -428,7 +504,7 @@ LimiterImpl::LimiterImpl(int sampleRate) { // void LimiterImpl::setThreshold(float threshold) { - const double OUT_CEILING = -0.3; + const double OUT_CEILING = -0.3; // cannot be 0.0, due to dither const double Q31_TO_Q15 = 32768 / 2147483648.0; // limiter threshold = -48dB to 0dB @@ -571,8 +647,8 @@ public: }; template -void LimiterMono::process(float* input, int16_t* output, int numFrames) -{ +void LimiterMono::process(float* input, int16_t* output, int numFrames) { + for (int n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain @@ -623,8 +699,8 @@ public: }; template -void LimiterStereo::process(float* input, int16_t* output, int numFrames) -{ +void LimiterStereo::process(float* input, int16_t* output, int numFrames) { + for (int n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain @@ -663,6 +739,71 @@ void LimiterStereo::process(float* input, int16_t* output, int numFrames) } } +// +// Limiter (quad) +// +template +class LimiterQuad : public LimiterImpl { + + PeakFilter _filter; + QuadDelay _delay; + +public: + LimiterQuad(int sampleRate) : LimiterImpl(sampleRate) {} + + // interleaved quad input/output + void process(float* input, int16_t* output, int numFrames) override; +}; + +template +void LimiterQuad::process(float* input, int16_t* output, int numFrames) { + + for (int n = 0; n < numFrames; n++) { + + // peak detect and convert to log2 domain + int32_t peak = peaklog2(&input[4*n+0], &input[4*n+1], &input[4*n+2], &input[4*n+3]); + + // compute limiter attenuation + int32_t attn = MAX(_threshold - peak, 0); + + // apply envelope + attn = envelope(attn); + + // convert from log2 domain + attn = fixexp2(attn); + + // lowpass filter + attn = _filter.process(attn); + float gain = attn * _outGain; + + // delay audio + float x0 = input[4*n+0]; + float x1 = input[4*n+1]; + float x2 = input[4*n+2]; + float x3 = input[4*n+3]; + _delay.process(x0, x1, x2, x3); + + // apply gain + x0 *= gain; + x1 *= gain; + x2 *= gain; + x3 *= gain; + + // apply dither + float d = dither(); + x0 += d; + x1 += d; + x2 += d; + x3 += d; + + // store 16-bit output + output[4*n+0] = (int16_t)floatToInt(x0); + output[4*n+1] = (int16_t)floatToInt(x1); + output[4*n+2] = (int16_t)floatToInt(x2); + output[4*n+3] = (int16_t)floatToInt(x3); + } +} + // // Public API // @@ -695,6 +836,19 @@ AudioLimiter::AudioLimiter(int sampleRate, int numChannels) { _impl = new LimiterStereo<128>(sampleRate); } + } else if (numChannels == 4) { + + // ~1.5ms lookahead for all rates + if (sampleRate < 16000) { + _impl = new LimiterQuad<16>(sampleRate); + } else if (sampleRate < 32000) { + _impl = new LimiterQuad<32>(sampleRate); + } else if (sampleRate < 64000) { + _impl = new LimiterQuad<64>(sampleRate); + } else { + _impl = new LimiterQuad<128>(sampleRate); + } + } else { assert(0); // unsupported } From f5d52c3d3be239680fd5d76caed0ee72ec3a3a21 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 24 Dec 2016 09:00:23 -0800 Subject: [PATCH 41/68] 64-bit code optimizations Use size_t for inner-loop array indexing, to avoid extraneous MOVSXD instructions when compiled with MSVC x64. --- libraries/audio/src/AudioLimiter.cpp | 54 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/libraries/audio/src/AudioLimiter.cpp b/libraries/audio/src/AudioLimiter.cpp index e0808c1daa..775e6f0c76 100644 --- a/libraries/audio/src/AudioLimiter.cpp +++ b/libraries/audio/src/AudioLimiter.cpp @@ -158,7 +158,7 @@ static inline int32_t peaklog2(float* input) { return 0x7fffffff; } - int k = x >> (31 - LOG2_TABBITS); + size_t k = x >> (31 - LOG2_TABBITS); // polynomial for log2(1+x) over x=[0,1] int32_t c0 = log2Table[k][0]; @@ -197,7 +197,7 @@ static inline int32_t peaklog2(float* input0, float* input1) { return 0x7fffffff; } - int k = x >> (31 - LOG2_TABBITS); + size_t k = x >> (31 - LOG2_TABBITS); // polynomial for log2(1+x) over x=[0,1] int32_t c0 = log2Table[k][0]; @@ -240,7 +240,7 @@ static inline int32_t peaklog2(float* input0, float* input1, float* input2, floa return 0x7fffffff; } - int k = x >> (31 - LOG2_TABBITS); + size_t k = x >> (31 - LOG2_TABBITS); // polynomial for log2(1+x) over x=[0,1] int32_t c0 = log2Table[k][0]; @@ -264,7 +264,7 @@ static inline int32_t fixexp2(int32_t x) { int32_t e = x >> LOG2_FRACBITS; x = ~(x << LOG2_INTBITS) & 0x7fffffff; - int k = x >> (31 - EXP2_TABBITS); + size_t k = x >> (31 - EXP2_TABBITS); // polynomial for exp2(x) int32_t c0 = exp2Table[k][0]; @@ -301,7 +301,7 @@ class PeakFilterT { static_assert((CIC1 - 1) + (CIC2 - 1) == (N - 1), "Total CIC delay must be N-1"); int32_t _buffer[2*N] = {}; // shared FIFO - int _index = 0; + size_t _index = 0; int32_t _acc1 = 0; // CIC1 integrator int32_t _acc2 = 0; // CIC2 integrator @@ -310,21 +310,21 @@ public: PeakFilterT() { // fill history - for (int n = 0; n < N-1; n++) { + for (size_t n = 0; n < N-1; n++) { process(0x7fffffff); } } int32_t process(int32_t x) { - const int MASK = 2*N - 1; // buffer wrap - int i = _index; + const size_t MASK = 2*N - 1; // buffer wrap + size_t i = _index; // Fast peak-hold using a running-min filter. Finds the peak (min) value // in the sliding window of N-1 samples, using only log2(N) comparisons. // Hold time of N-1 samples exactly cancels the step response of FIR filter. - for (int n = 1; n < N; n <<= 1) { + for (size_t n = 1; n < N; n <<= 1) { _buffer[i] = x; i = (i + n) & MASK; @@ -372,13 +372,13 @@ class MonoDelay { static_assert((N & (N - 1)) == 0, "N must be a power of 2"); float _buffer[N] = {}; - int _index = 0; + size_t _index = 0; public: void process(float& x) { - const int MASK = N - 1; // buffer wrap - int i = _index; + const size_t MASK = N - 1; // buffer wrap + size_t i = _index; _buffer[i] = x; @@ -399,13 +399,13 @@ class StereoDelay { static_assert((N & (N - 1)) == 0, "N must be a power of 2"); float _buffer[2*N] = {}; - int _index = 0; + size_t _index = 0; public: void process(float& x0, float& x1) { - const int MASK = 2*N - 1; // buffer wrap - int i = _index; + const size_t MASK = 2*N - 1; // buffer wrap + size_t i = _index; _buffer[i+0] = x0; _buffer[i+1] = x1; @@ -428,13 +428,13 @@ class QuadDelay { static_assert((N & (N - 1)) == 0, "N must be a power of 2"); float _buffer[4*N] = {}; - int _index = 0; + size_t _index = 0; public: void process(float& x0, float& x1, float& x2, float& x3) { - const int MASK = 4*N - 1; // buffer wrap - int i = _index; + const size_t MASK = 4*N - 1; // buffer wrap + size_t i = _index; _buffer[i+0] = x0; _buffer[i+1] = x1; @@ -547,7 +547,7 @@ void LimiterImpl::setRelease(float release) { double x = MAXHOLD * _sampleRate; double xstep = x / NHOLD; // 1.0 to 1.0/NHOLD - int i = 0; + size_t i = 0; for (; i < NHOLD; i++) { // max release @@ -613,12 +613,12 @@ int32_t LimiterImpl::envelope(int32_t attn) { // arc = (attn-rms)*6/attn for attn = 1dB to 6dB // arc = (attn-rms)*6/6 for attn > 6dB - int bits = MIN(attn >> 20, 0x3f); // saturate 1/attn at 6dB - _arc = MAX(attn - _rms, 0); // peak/rms = (attn-rms) - _arc = MULHI(_arc, invTable[bits]); // normalized peak/rms = (attn-rms)/attn - _arc = MIN(_arc, NARC - 1); // saturate at 6dB + size_t bits = MIN(attn >> 20, 0x3f); // saturate 1/attn at 6dB + _arc = MAX(attn - _rms, 0); // peak/rms = (attn-rms) + _arc = MULHI(_arc, invTable[bits]); // normalized peak/rms = (attn-rms)/attn + _arc = MIN(_arc, NARC - 1); // saturate at 6dB - _arcRelease = 0x7fffffff; // reset release + _arcRelease = 0x7fffffff; // reset release } _attn = attn; @@ -649,7 +649,7 @@ public: template void LimiterMono::process(float* input, int16_t* output, int numFrames) { - for (int n = 0; n < numFrames; n++) { + for (size_t n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain int32_t peak = peaklog2(&input[n]); @@ -701,7 +701,7 @@ public: template void LimiterStereo::process(float* input, int16_t* output, int numFrames) { - for (int n = 0; n < numFrames; n++) { + for (size_t n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain int32_t peak = peaklog2(&input[2*n+0], &input[2*n+1]); @@ -758,7 +758,7 @@ public: template void LimiterQuad::process(float* input, int16_t* output, int numFrames) { - for (int n = 0; n < numFrames; n++) { + for (size_t n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain int32_t peak = peaklog2(&input[4*n+0], &input[4*n+1], &input[4*n+2], &input[4*n+3]); From fc2c2a190652680b008d982ac0fdd3e01ac71992 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 24 Dec 2016 10:06:30 -0800 Subject: [PATCH 42/68] fix compiler warnings --- libraries/audio/src/AudioLimiter.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/audio/src/AudioLimiter.cpp b/libraries/audio/src/AudioLimiter.cpp index 775e6f0c76..316c0b181a 100644 --- a/libraries/audio/src/AudioLimiter.cpp +++ b/libraries/audio/src/AudioLimiter.cpp @@ -158,7 +158,7 @@ static inline int32_t peaklog2(float* input) { return 0x7fffffff; } - size_t k = x >> (31 - LOG2_TABBITS); + int k = x >> (31 - LOG2_TABBITS); // polynomial for log2(1+x) over x=[0,1] int32_t c0 = log2Table[k][0]; @@ -197,7 +197,7 @@ static inline int32_t peaklog2(float* input0, float* input1) { return 0x7fffffff; } - size_t k = x >> (31 - LOG2_TABBITS); + int k = x >> (31 - LOG2_TABBITS); // polynomial for log2(1+x) over x=[0,1] int32_t c0 = log2Table[k][0]; @@ -240,7 +240,7 @@ static inline int32_t peaklog2(float* input0, float* input1, float* input2, floa return 0x7fffffff; } - size_t k = x >> (31 - LOG2_TABBITS); + int k = x >> (31 - LOG2_TABBITS); // polynomial for log2(1+x) over x=[0,1] int32_t c0 = log2Table[k][0]; @@ -264,7 +264,7 @@ static inline int32_t fixexp2(int32_t x) { int32_t e = x >> LOG2_FRACBITS; x = ~(x << LOG2_INTBITS) & 0x7fffffff; - size_t k = x >> (31 - EXP2_TABBITS); + int k = x >> (31 - EXP2_TABBITS); // polynomial for exp2(x) int32_t c0 = exp2Table[k][0]; @@ -547,7 +547,7 @@ void LimiterImpl::setRelease(float release) { double x = MAXHOLD * _sampleRate; double xstep = x / NHOLD; // 1.0 to 1.0/NHOLD - size_t i = 0; + int i = 0; for (; i < NHOLD; i++) { // max release @@ -649,7 +649,7 @@ public: template void LimiterMono::process(float* input, int16_t* output, int numFrames) { - for (size_t n = 0; n < numFrames; n++) { + for (int n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain int32_t peak = peaklog2(&input[n]); @@ -701,7 +701,7 @@ public: template void LimiterStereo::process(float* input, int16_t* output, int numFrames) { - for (size_t n = 0; n < numFrames; n++) { + for (int n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain int32_t peak = peaklog2(&input[2*n+0], &input[2*n+1]); @@ -758,7 +758,7 @@ public: template void LimiterQuad::process(float* input, int16_t* output, int numFrames) { - for (size_t n = 0; n < numFrames; n++) { + for (int n = 0; n < numFrames; n++) { // peak detect and convert to log2 domain int32_t peak = peaklog2(&input[4*n+0], &input[4*n+1], &input[4*n+2], &input[4*n+3]); From a2ea6bf36e622912cd3bbc5277d05fa315a7b8c9 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Sat, 24 Dec 2016 11:10:35 -0800 Subject: [PATCH 43/68] Improved audio resampler Added LQ mode (2x faster). Added HQ mode (2x slower), intended for offline resampling. Default (MQ) quality is slightly improved (512 filter phases in irrational mode). --- libraries/audio/src/AudioSRC.cpp | 31 +- libraries/audio/src/AudioSRC.h | 14 +- libraries/audio/src/AudioSRCData.h | 3663 ++++++++++++++++++++++++---- 3 files changed, 3164 insertions(+), 544 deletions(-) diff --git a/libraries/audio/src/AudioSRC.cpp b/libraries/audio/src/AudioSRC.cpp index 3cd7a53b6b..80cb756d04 100644 --- a/libraries/audio/src/AudioSRC.cpp +++ b/libraries/audio/src/AudioSRC.cpp @@ -99,18 +99,22 @@ static void cubicInterpolation(const float* input, float* output, int inputSize, } } -int AudioSRC::createRationalFilter(int upFactor, int downFactor, float gain) { - int numTaps = PROTOTYPE_TAPS; +int AudioSRC::createRationalFilter(int upFactor, int downFactor, float gain, Quality quality) { + + int prototypeTaps = prototypeFilterTable[quality].taps; + int prototypeCoefs = prototypeFilterTable[quality].coefs; + const float* prototypeFilter = prototypeFilterTable[quality].filter; + + int numTaps = prototypeTaps; int numPhases = upFactor; int numCoefs = numTaps * numPhases; - int oldCoefs = numCoefs; - int prototypeCoefs = PROTOTYPE_TAPS * PROTOTYPE_PHASES; // // When downsampling, we can lower the filter cutoff by downFactor/upFactor using the // time-scaling property of the Fourier transform. The gain is adjusted accordingly. // if (downFactor > upFactor) { + int oldCoefs = numCoefs; numCoefs = ((int64_t)oldCoefs * downFactor) / upFactor; numTaps = (numCoefs + upFactor - 1) / upFactor; gain *= (float)oldCoefs / numCoefs; @@ -149,18 +153,22 @@ int AudioSRC::createRationalFilter(int upFactor, int downFactor, float gain) { return numTaps; } -int AudioSRC::createIrrationalFilter(int upFactor, int downFactor, float gain) { - int numTaps = PROTOTYPE_TAPS; +int AudioSRC::createIrrationalFilter(int upFactor, int downFactor, float gain, Quality quality) { + + int prototypeTaps = prototypeFilterTable[quality].taps; + int prototypeCoefs = prototypeFilterTable[quality].coefs; + const float* prototypeFilter = prototypeFilterTable[quality].filter; + + int numTaps = prototypeTaps; int numPhases = upFactor; int numCoefs = numTaps * numPhases; - int oldCoefs = numCoefs; - int prototypeCoefs = PROTOTYPE_TAPS * PROTOTYPE_PHASES; // // When downsampling, we can lower the filter cutoff by downFactor/upFactor using the // time-scaling property of the Fourier transform. The gain is adjusted accordingly. // if (downFactor > upFactor) { + int oldCoefs = numCoefs; numCoefs = ((int64_t)oldCoefs * downFactor) / upFactor; numTaps = (numCoefs + upFactor - 1) / upFactor; gain *= (float)oldCoefs / numCoefs; @@ -1405,7 +1413,8 @@ int AudioSRC::render(float** inputs, float** outputs, int inputFrames) { return outputFrames; } -AudioSRC::AudioSRC(int inputSampleRate, int outputSampleRate, int numChannels) { +AudioSRC::AudioSRC(int inputSampleRate, int outputSampleRate, int numChannels, Quality quality) { + assert(inputSampleRate > 0); assert(outputSampleRate > 0); assert(numChannels > 0); @@ -1433,9 +1442,9 @@ AudioSRC::AudioSRC(int inputSampleRate, int outputSampleRate, int numChannels) { // create the polyphase filter if (_step == 0) { - _numTaps = createRationalFilter(_upFactor, _downFactor, 1.0f); + _numTaps = createRationalFilter(_upFactor, _downFactor, 1.0f, quality); } else { - _numTaps = createIrrationalFilter(_upFactor, _downFactor, 1.0f); + _numTaps = createIrrationalFilter(_upFactor, _downFactor, 1.0f, quality); } //printf("up=%d down=%.3f taps=%d\n", _upFactor, _downFactor + (LO32(_step)< Date: Mon, 26 Dec 2016 20:16:05 -0800 Subject: [PATCH 44/68] quiet some warnings --- plugins/steamClient/src/SteamAPIPlugin.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/steamClient/src/SteamAPIPlugin.h b/plugins/steamClient/src/SteamAPIPlugin.h index 90fda06de3..0305a2d23f 100644 --- a/plugins/steamClient/src/SteamAPIPlugin.h +++ b/plugins/steamClient/src/SteamAPIPlugin.h @@ -19,19 +19,19 @@ class QUrl; class SteamAPIPlugin : public SteamClientPlugin { public: - bool isRunning(); + bool isRunning() override; - bool init(); - void shutdown(); + bool init() override; + void shutdown() override; - void runCallbacks(); + void runCallbacks() override; - void requestTicket(TicketRequestCallback callback); - void updateLocation(QString status, QUrl locationUrl); - void openInviteOverlay(); - void joinLobby(QString lobbyId); + void requestTicket(TicketRequestCallback callback) override; + void updateLocation(QString status, QUrl locationUrl) override; + void openInviteOverlay() override; + void joinLobby(QString lobbyId) override; - int getSteamVRBuildID(); + int getSteamVRBuildID() override; }; #endif // hifi_SteamAPIPlugin_h From e674be9c8c940a91cad01af506fbf22a5000d8d9 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 27 Dec 2016 15:15:11 -0800 Subject: [PATCH 45/68] fix animated entities --- libraries/animation/src/Rig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 6066905943..0e14beab87 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -924,8 +924,8 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { for (auto& trigger : triggersOut) { _animVars.setTrigger(trigger); } - applyOverridePoses(); } + applyOverridePoses(); buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); // copy internal poses to external poses From 901c020aaeaef8b471c5149b4da28398561639e5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 26 Dec 2016 13:57:51 -0800 Subject: [PATCH 46/68] Optimizations, SIMD and const correctness --- libraries/render-utils/src/Model.cpp | 11 ++++++-- libraries/render/src/render/Scene.cpp | 4 +-- libraries/render/src/render/Scene.h | 2 +- libraries/shared/src/AABox.cpp | 10 +++---- libraries/shared/src/GLMHelpers.cpp | 5 +++- tests/shared/src/GLMHelpersTests.cpp | 38 +++++++++++++++++++++++++++ tests/shared/src/GLMHelpersTests.h | 1 + 7 files changed, 60 insertions(+), 11 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 436574a1ff..ffbe1fb34c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1160,7 +1160,8 @@ void Model::updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrient } _needsUpdateClusterMatrices = false; const FBXGeometry& geometry = getFBXGeometry(); - glm::mat4 zeroScale(glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), + static const glm::mat4 zeroScale( + glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); @@ -1170,11 +1171,17 @@ void Model::updateClusterMatrices(glm::vec3 modelPosition, glm::quat modelOrient for (int i = 0; i < _meshStates.size(); i++) { MeshState& state = _meshStates[i]; const FBXMesh& mesh = geometry.meshes.at(i); - for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); auto jointMatrix = _rig->getJointTransform(cluster.jointIndex); +#if GLM_ARCH & GLM_ARCH_SSE2 + glm::mat4 temp, out, inverseBindMatrix = cluster.inverseBindMatrix; + glm_mat4_mul((glm_vec4*)&modelToWorld, (glm_vec4*)&jointMatrix, (glm_vec4*)&temp); + glm_mat4_mul((glm_vec4*)&temp, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); + state.clusterMatrices[j] = out; +#else state.clusterMatrices[j] = modelToWorld * jointMatrix * cluster.inverseBindMatrix; +#endif // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index f918fc0bf6..95fef3e9f0 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -35,7 +35,7 @@ void PendingChanges::updateItem(ItemID id, const UpdateFunctorPointer& functor) _updateFunctors.push_back(functor); } -void PendingChanges::merge(PendingChanges& changes) { +void PendingChanges::merge(const PendingChanges& changes) { _resetItems.insert(_resetItems.end(), changes._resetItems.begin(), changes._resetItems.end()); _resetPayloads.insert(_resetPayloads.end(), changes._resetPayloads.begin(), changes._resetPayloads.end()); _removedItems.insert(_removedItems.end(), changes._removedItems.begin(), changes._removedItems.end()); @@ -71,7 +71,7 @@ void Scene::enqueuePendingChanges(const PendingChanges& pendingChanges) { void consolidateChangeQueue(PendingChangesQueue& queue, PendingChanges& singleBatch) { while (!queue.empty()) { - auto pendingChanges = queue.front(); + const auto& pendingChanges = queue.front(); singleBatch.merge(pendingChanges); queue.pop(); }; diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index 6b57a22a36..13475d0556 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -34,7 +34,7 @@ public: void updateItem(ItemID id, const UpdateFunctorPointer& functor); void updateItem(ItemID id) { updateItem(id, nullptr); } - void merge(PendingChanges& changes); + void merge(const PendingChanges& changes); ItemIDs _resetItems; Payloads _resetPayloads; diff --git a/libraries/shared/src/AABox.cpp b/libraries/shared/src/AABox.cpp index 5e9c031355..4a74fb4033 100644 --- a/libraries/shared/src/AABox.cpp +++ b/libraries/shared/src/AABox.cpp @@ -575,18 +575,18 @@ void AABox::transform(const Transform& transform) { // Logic based on http://clb.demon.fi/MathGeoLib/nightly/docs/AABB.cpp_code.html#471 void AABox::transform(const glm::mat4& matrix) { + // FIXME use simd operations auto halfSize = _scale * 0.5f; auto center = _corner + halfSize; halfSize = abs(halfSize); - auto newCenter = transformPoint(matrix, center); - auto mm = glm::transpose(glm::mat3(matrix)); vec3 newDir = vec3( - glm::dot(glm::abs(vec3(mm[0])), halfSize), - glm::dot(glm::abs(vec3(mm[1])), halfSize), - glm::dot(glm::abs(vec3(mm[2])), halfSize) + glm::dot(glm::abs(mm[0]), halfSize), + glm::dot(glm::abs(mm[1]), halfSize), + glm::dot(glm::abs(mm[2]), halfSize) ); + auto newCenter = transformPoint(matrix, center); _corner = newCenter - newDir; _scale = newDir * 2.0f; } diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 3520a4dc44..6aa4f68f22 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -502,7 +502,10 @@ glm::mat4 cancelOutRollAndPitch(const glm::mat4& m) { glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p) { glm::vec4 temp = m * glm::vec4(p, 1.0f); - return glm::vec3(temp.x / temp.w, temp.y / temp.w, temp.z / temp.w); + if (temp.w != 1.0f) { + temp *= (1.0f / temp.w); + } + return glm::vec3(temp); } // does not handle non-uniform scale correctly, but it's faster then transformVectorFull diff --git a/tests/shared/src/GLMHelpersTests.cpp b/tests/shared/src/GLMHelpersTests.cpp index a796d62ba5..8d26d35c69 100644 --- a/tests/shared/src/GLMHelpersTests.cpp +++ b/tests/shared/src/GLMHelpersTests.cpp @@ -15,6 +15,8 @@ #include #include <../QTestExtensions.h> +#include +#include QTEST_MAIN(GLMHelpersTests) @@ -102,3 +104,39 @@ void GLMHelpersTests::testSixByteOrientationCompression() { testQuatCompression(-(ROT_Y_180 * ROT_Z_30 * ROT_X_90)); testQuatCompression(-(ROT_Z_30 * ROT_X_90 * ROT_Y_180)); } + +#define LOOPS 500000 + +void GLMHelpersTests::testSimd() { + glm::mat4 a = glm::translate(glm::mat4(), vec3(1, 4, 9)); + glm::mat4 b = glm::rotate(glm::mat4(), PI / 3, vec3(0, 1, 0)); + glm::mat4 a1, b1; + glm::mat4 a2, b2; + + a1 = a * b; + b1 = b * a; + glm_mat4_mul((glm_vec4*)&a, (glm_vec4*)&b, (glm_vec4*)&a2); + glm_mat4_mul((glm_vec4*)&b, (glm_vec4*)&a, (glm_vec4*)&b2); + + + { + QElapsedTimer timer; + timer.start(); + for (size_t i = 0; i < LOOPS; ++i) { + a1 = a * b; + b1 = b * a; + } + qDebug() << "Native " << timer.elapsed(); + } + + { + QElapsedTimer timer; + timer.start(); + for (size_t i = 0; i < LOOPS; ++i) { + glm_mat4_mul((glm_vec4*)&a, (glm_vec4*)&b, (glm_vec4*)&a2); + glm_mat4_mul((glm_vec4*)&b, (glm_vec4*)&a, (glm_vec4*)&b2); + } + qDebug() << "SIMD " << timer.elapsed(); + } + qDebug() << "Done "; +} diff --git a/tests/shared/src/GLMHelpersTests.h b/tests/shared/src/GLMHelpersTests.h index 40d552a07b..acc7b533f5 100644 --- a/tests/shared/src/GLMHelpersTests.h +++ b/tests/shared/src/GLMHelpersTests.h @@ -20,6 +20,7 @@ class GLMHelpersTests : public QObject { private slots: void testEulerDecomposition(); void testSixByteOrientationCompression(); + void testSimd(); }; float getErrorDifference(const float& a, const float& b); From 0bb82d2b73614b31fa703ad0e5ec45fcfecd5f01 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 28 Dec 2016 16:04:22 -0500 Subject: [PATCH 47/68] use libquazip5.so in debug --- cmake/externals/quazip/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt index f00403640a..b8b3fe43d8 100644 --- a/cmake/externals/quazip/CMakeLists.txt +++ b/cmake/externals/quazip/CMakeLists.txt @@ -38,13 +38,13 @@ set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/lib CACHE FILEPATH "Location if (APPLE) set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip5.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") - set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5d.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") elseif (WIN32) set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/quazip5.lib CACHE FILEPATH "Location of QuaZip release library") - set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/quazip5d.lib CACHE FILEPATH "Location of QuaZip release library") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/quazip5.lib CACHE FILEPATH "Location of QuaZip release library") else () set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip5.so CACHE FILEPATH "Location of QuaZip release library") - set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5d.so CACHE FILEPATH "Location of QuaZip release library") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5.so CACHE FILEPATH "Location of QuaZip release library") endif () include(SelectLibraryConfigurations) @@ -52,4 +52,4 @@ select_library_configurations(${EXTERNAL_NAME_UPPER}) # Force selected libraries into the cache set(${EXTERNAL_NAME_UPPER}_LIBRARY ${${EXTERNAL_NAME_UPPER}_LIBRARY} CACHE FILEPATH "Location of QuaZip libraries") -set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries") \ No newline at end of file +set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries") From f79c6e08f5c40aa0d8c07cd428a6325760fe3e62 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 28 Dec 2016 14:18:23 -0800 Subject: [PATCH 48/68] Track dropped frames as reported by the Oculus SDK --- interface/src/Application.cpp | 1 + libraries/plugins/src/plugins/DisplayPlugin.h | 4 ++++ plugins/oculus/src/OculusDisplayPlugin.cpp | 14 +++++++++++--- plugins/oculus/src/OculusDisplayPlugin.h | 4 ++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d92ba53f6e..eb2f6f5896 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1271,6 +1271,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["sim_rate"] = getAverageSimsPerSecond(); properties["avatar_sim_rate"] = getAvatarSimrate(); properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); + properties["hardware_stats"] = displayPlugin->getHardwareStats(); auto bandwidthRecorder = DependencyManager::get(); properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 6b55d8ed64..2491aed817 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -194,6 +195,9 @@ public: virtual float newFramePresentRate() const { return -1.0f; } // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } + + // Hardware specific stats + virtual QJsonObject getHardwareStats() const { return QJsonObject(); } uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 88ca02edc2..7a56124298 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -147,19 +147,27 @@ void OculusDisplayPlugin::hmdPresent() { logWarning("Failed to present"); } - static int droppedFrames = 0; + static int compositorDroppedFrames = 0; + static int appDroppedFrames = 0; ovrPerfStats perfStats; ovr_GetPerfStats(_session, &perfStats); for (int i = 0; i < perfStats.FrameStatsCount; ++i) { const auto& frameStats = perfStats.FrameStats[i]; - int delta = frameStats.CompositorDroppedFrameCount - droppedFrames; + int delta = frameStats.CompositorDroppedFrameCount - compositorDroppedFrames; _stutterRate.increment(delta); - droppedFrames = frameStats.CompositorDroppedFrameCount; + compositorDroppedFrames = frameStats.CompositorDroppedFrameCount; + appDroppedFrames = frameStats.AppDroppedFrameCount; } + _hardwareStats["app_dropped_frame_count"] = appDroppedFrames; } _presentRate.increment(); } + +QJsonObject OculusDisplayPlugin::getHardwareStats() const { + return _hardwareStats; +} + bool OculusDisplayPlugin::isHmdMounted() const { ovrSessionStatus status; return (OVR_SUCCESS(ovr_GetSessionStatus(_session, &status)) && diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index fce8e9e6ce..a953e01e2f 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -19,6 +19,8 @@ public: QString getPreferredAudioInDevice() const override; QString getPreferredAudioOutDevice() const override; + + virtual QJsonObject getHardwareStats() const; protected: bool internalActivate() override; @@ -33,5 +35,7 @@ private: ovrTextureSwapChain _textureSwapChain; gpu::FramebufferPointer _outputFramebuffer; bool _customized { false }; + + QJsonObject _hardwareStats; }; From 522403cf48390343b7df40cfcdbce8fec3d31514 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 28 Dec 2016 18:08:17 -0800 Subject: [PATCH 49/68] fix build error on linux --- tests/shared/src/TraceTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shared/src/TraceTests.cpp b/tests/shared/src/TraceTests.cpp index 5536d17ca6..a3ef2e8725 100644 --- a/tests/shared/src/TraceTests.cpp +++ b/tests/shared/src/TraceTests.cpp @@ -28,7 +28,7 @@ void TraceTests::testTraceSerialization() { auto start = usecTimestampNow(); PROFILE_RANGE(test, "TestEvent") for (size_t i = 0; i < 10000; ++i) { - SAMPLE_PROFILE_COUNTER(0.1f, test, "TestCounter", { { "i", i } }) + SAMPLE_PROFILE_COUNTER(0.1f, test, "TestCounter", { { "i", (int)i } }) } auto duration = usecTimestampNow() - start; duration /= USECS_PER_MSEC; From 9763566bdb73c7cf63d4ca7c4630c1747e295849 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 28 Dec 2016 18:09:31 -0800 Subject: [PATCH 50/68] fix bitrot from April 2016 --- tests/shared/src/MovingPercentileTests.cpp | 34 ++++++++++++---------- tests/shared/src/MovingPercentileTests.h | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/shared/src/MovingPercentileTests.cpp b/tests/shared/src/MovingPercentileTests.cpp index fbbc3c7b9e..741c966525 100644 --- a/tests/shared/src/MovingPercentileTests.cpp +++ b/tests/shared/src/MovingPercentileTests.cpp @@ -39,19 +39,21 @@ void MovingPercentileTests::testRunningMedian() { } -float MovingPercentileTests::random() { - return rand() / (float)RAND_MAX; +int64_t MovingPercentileTests::random() { + return ((int64_t) rand() << 48) ^ + ((int64_t) rand() << 32) ^ + ((int64_t) rand() << 16) ^ + ((int64_t) rand()); } void MovingPercentileTests::testRunningMinForN (int n) { - // Stores the last n samples - QQueue samples; + QQueue samples; MovingPercentile movingMin (n, 0.0f); for (int s = 0; s < 3 * n; ++s) { - float sample = random(); + int64_t sample = random(); samples.push_back(sample); if (samples.size() > n) @@ -64,30 +66,32 @@ void MovingPercentileTests::testRunningMinForN (int n) { movingMin.updatePercentile(sample); // Calculate the minimum of the moving samples - float expectedMin = std::numeric_limits::max(); + int64_t expectedMin = std::numeric_limits::max(); int prevSize = samples.size(); - for (auto val : samples) + for (auto val : samples) { expectedMin = std::min(val, expectedMin); + } QCOMPARE(samples.size(), prevSize); - QCOMPARE(movingMin.getValueAtPercentile(), expectedMin); + QVERIFY(movingMin.getValueAtPercentile() - expectedMin == 0L); } } void MovingPercentileTests::testRunningMaxForN (int n) { // Stores the last n samples - QQueue samples; + QQueue samples; MovingPercentile movingMax (n, 1.0f); for (int s = 0; s < 10000; ++s) { - float sample = random(); + int64_t sample = random(); samples.push_back(sample); - if (samples.size() > n) + if (samples.size() > n) { samples.pop_front(); + } if (samples.size() == 0) { QFAIL_WITH_MESSAGE("\n\n\n\tWTF\n\tsamples.size() = " << samples.size() << ", n = " << n); @@ -96,22 +100,22 @@ void MovingPercentileTests::testRunningMaxForN (int n) { movingMax.updatePercentile(sample); // Calculate the maximum of the moving samples - float expectedMax = std::numeric_limits::min(); + int64_t expectedMax = std::numeric_limits::min(); for (auto val : samples) expectedMax = std::max(val, expectedMax); - QCOMPARE(movingMax.getValueAtPercentile(), expectedMax); + QVERIFY(movingMax.getValueAtPercentile() - expectedMax == 0L); } } void MovingPercentileTests::testRunningMedianForN (int n) { // Stores the last n samples - QQueue samples; + QQueue samples; MovingPercentile movingMedian (n, 0.5f); for (int s = 0; s < 10000; ++s) { - float sample = random(); + int64_t sample = random(); samples.push_back(sample); if (samples.size() > n) diff --git a/tests/shared/src/MovingPercentileTests.h b/tests/shared/src/MovingPercentileTests.h index ffc8ddb0f6..d9c96d2752 100644 --- a/tests/shared/src/MovingPercentileTests.h +++ b/tests/shared/src/MovingPercentileTests.h @@ -25,7 +25,7 @@ private slots: private: // Utilities and helper functions - float random(); + int64_t random(); void testRunningMinForN (int n); void testRunningMaxForN (int n); void testRunningMedianForN (int n); From 39538db63afd75a0c2562740dc726cd640e61cb5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 28 Dec 2016 18:10:45 -0800 Subject: [PATCH 51/68] fix bitrot from changed glm::vec3 API? --- tests/shared/src/GeometryUtilTests.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/shared/src/GeometryUtilTests.cpp b/tests/shared/src/GeometryUtilTests.cpp index 79472a1128..ccb3dc8a0e 100644 --- a/tests/shared/src/GeometryUtilTests.cpp +++ b/tests/shared/src/GeometryUtilTests.cpp @@ -255,11 +255,11 @@ void GeometryUtilTests::testTwistSwingDecomposition() { glm::quat measuredTwistRotation; glm::quat measuredSwingRotation; swingTwistDecomposition(totalRotation, twistAxis, measuredSwingRotation, measuredTwistRotation); - + // dot decomposed with components float twistDot = fabsf(glm::dot(twistRotation, measuredTwistRotation)); float swingDot = fabsf(glm::dot(swingRotation, measuredSwingRotation)); - + // the dot products should be very close to 1.0 const float MIN_ERROR = 1.0e-6f; QCOMPARE_WITH_ABS_ERROR(1.0f, twistDot, MIN_ERROR); @@ -277,7 +277,7 @@ void GeometryUtilTests::testSphereCapsulePenetration() { glm::vec3 capsuleEnd(0.0f, 10.0f, 0.0f); float capsuleRadius = 1.0f; - glm::vec3 penetration(glm::vec3::_null); + glm::vec3 penetration(0.0f); bool hit = findSphereCapsulePenetration(sphereCenter, sphereRadius, capsuleStart, capsuleEnd, capsuleRadius, penetration); QCOMPARE(hit, true); QCOMPARE_WITH_ABS_ERROR(penetration, glm::vec3(-0.5f, 0.0f, 0.0f), EPSILON); From 96a9ef8f44dde9669ae17fa3db354062b1e25d6d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 28 Dec 2016 18:11:54 -0800 Subject: [PATCH 52/68] fix bitrot after UDT overhaul --- tests/networking/src/PacketTests.cpp | 30 +++++++++++------------ tests/networking/src/ResourceTests.cpp | 33 +++++++------------------- 2 files changed, 23 insertions(+), 40 deletions(-) diff --git a/tests/networking/src/PacketTests.cpp b/tests/networking/src/PacketTests.cpp index cbb949aa84..ff2149930e 100644 --- a/tests/networking/src/PacketTests.cpp +++ b/tests/networking/src/PacketTests.cpp @@ -12,29 +12,29 @@ #include "PacketTests.h" #include "../QTestExtensions.h" -#include +#include QTEST_MAIN(PacketTests) -std::unique_ptr copyToReadPacket(std::unique_ptr& packet) { +std::unique_ptr copyToReadPacket(std::unique_ptr& packet) { auto size = packet->getDataSize(); auto data = std::unique_ptr(new char[size]); memcpy(data.get(), packet->getData(), size); - return Packet::fromReceivedPacket(std::move(data), size, HifiSockAddr()); + return NLPacket::fromReceivedPacket(std::move(data), size, HifiSockAddr()); } void PacketTests::emptyPacketTest() { - auto packet = Packet::create(PacketType::Unknown); + auto packet = NLPacket::create(PacketType::Unknown); QCOMPARE(packet->getType(), PacketType::Unknown); QCOMPARE(packet->getPayloadSize(), 0); - QCOMPARE(packet->getDataSize(), packet->totalHeadersSize()); + QCOMPARE(packet->getDataSize(), NLPacket::totalHeaderSize(packet->getType())); QCOMPARE(packet->bytesLeftToRead(), 0); QCOMPARE(packet->bytesAvailableForWrite(), packet->getPayloadCapacity()); } void PacketTests::packetTypeTest() { - auto packet = Packet::create(PacketType::EntityAdd); + auto packet = NLPacket::create(PacketType::EntityAdd); QCOMPARE(packet->getType(), PacketType::EntityAdd); @@ -46,7 +46,7 @@ void PacketTests::packetTypeTest() { } void PacketTests::writeTest() { - auto packet = Packet::create(PacketType::Unknown); + auto packet = NLPacket::create(PacketType::Unknown); QCOMPARE(packet->getPayloadSize(), 0); @@ -62,7 +62,7 @@ void PacketTests::writeTest() { void PacketTests::readTest() { // Test reads for several different size packets for (int i = 1; i < 4; i++) { - auto packet = Packet::create(PacketType::Unknown); + auto packet = NLPacket::create(PacketType::Unknown); auto size = packet->getPayloadCapacity(); size /= i; @@ -91,7 +91,7 @@ void PacketTests::readTest() { } void PacketTests::writePastCapacityTest() { - auto packet = Packet::create(PacketType::Unknown); + auto packet = NLPacket::create(PacketType::Unknown); auto size = packet->getPayloadCapacity(); char* data = new char[size]; @@ -111,20 +111,20 @@ void PacketTests::writePastCapacityTest() { QCOMPARE(packet->bytesAvailableForWrite(), 0); QCOMPARE(packet->getPayloadSize(), size); - QCOMPARE(Packet::PACKET_WRITE_ERROR, packet->write("data")); - - // Packet::write() shouldn't allow the caller to write if no space is left + QCOMPARE(NLPacket::PACKET_WRITE_ERROR, packet->write("data")); // asserts in DEBUG + + // NLPacket::write() shouldn't allow the caller to write if no space is left QCOMPARE(packet->getPayloadSize(), size); } void PacketTests::primitiveTest() { - auto packet = Packet::create(PacketType::Unknown); + auto packet = NLPacket::create(PacketType::Unknown); int value1 = 5; char value2 = 10; bool value3 = true; qint64 value4 = -93404; - + packet->writePrimitive(value1); packet->writePrimitive(value2); packet->writePrimitive(value3); @@ -133,7 +133,7 @@ void PacketTests::primitiveTest() { auto recvPacket = copyToReadPacket(packet); // Peek & read first value - { + { int peekValue = 0; QCOMPARE(recvPacket->peekPrimitive(&peekValue), (int)sizeof(peekValue)); QCOMPARE(peekValue, value1); diff --git a/tests/networking/src/ResourceTests.cpp b/tests/networking/src/ResourceTests.cpp index e6dcf18230..a96abc1b0c 100644 --- a/tests/networking/src/ResourceTests.cpp +++ b/tests/networking/src/ResourceTests.cpp @@ -37,25 +37,10 @@ void ResourceTests::initTestCase() { static QSharedPointer resource; -static bool waitForSignal(QObject *sender, const char *signal, int timeout = 1000) { - QEventLoop loop; - QTimer timer; - timer.setInterval(timeout); - timer.setSingleShot(true); - - loop.connect(sender, signal, SLOT(quit())); - loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); - timer.start(); - loop.exec(); - - return timer.isActive(); -} - void ResourceTests::downloadFirst() { - // download the Mery fst file QUrl meryUrl = QUrl("http://hifi-public.s3.amazonaws.com/marketplace/contents/e21c0b95-e502-4d15-8c41-ea2fc40f1125/3585ddf674869a67d31d5964f7b52de1.fst"); - resource = QSharedPointer::create(meryUrl, false); + resource = QSharedPointer::create(meryUrl); resource->setSelf(resource); const int timeout = 1000; @@ -64,9 +49,9 @@ void ResourceTests::downloadFirst() { timer.setInterval(timeout); timer.setSingleShot(true); - loop.connect(resource, SIGNAL(loaded(QNetworkReply&)), SLOT(quit())); - loop.connect(resource, SIGNAL(failed(QNetworkReply::NetworkError)), SLOT(quit())); - loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + connect(resource.data(), &Resource::loaded, &loop, &QEventLoop::quit); + connect(resource.data(), &Resource::failed, &loop, &QEventLoop::quit); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); timer.start(); resource->ensureLoading(); @@ -76,10 +61,9 @@ void ResourceTests::downloadFirst() { } void ResourceTests::downloadAgain() { - // download the Mery fst file QUrl meryUrl = QUrl("http://hifi-public.s3.amazonaws.com/marketplace/contents/e21c0b95-e502-4d15-8c41-ea2fc40f1125/3585ddf674869a67d31d5964f7b52de1.fst"); - resource = QSharedPointer::create(meryUrl, false); + resource = QSharedPointer::create(meryUrl); resource->setSelf(resource); const int timeout = 1000; @@ -88,14 +72,13 @@ void ResourceTests::downloadAgain() { timer.setInterval(timeout); timer.setSingleShot(true); - loop.connect(resource, SIGNAL(loaded(QNetworkReply&)), SLOT(quit())); - loop.connect(resource, SIGNAL(failed(QNetworkReply::NetworkError)), SLOT(quit())); - loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + connect(resource.data(), &Resource::loaded, &loop, &QEventLoop::quit); + connect(resource.data(), &Resource::failed, &loop, &QEventLoop::quit); + connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); timer.start(); resource->ensureLoading(); loop.exec(); QVERIFY(resource->isLoaded()); - } From 1e38170ba9a17d498c1a32ac4526e466a77cc897 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 28 Dec 2016 18:12:22 -0800 Subject: [PATCH 53/68] fix bitrot from AudioRingBuffer API change --- tests/audio/src/AudioRingBufferTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/audio/src/AudioRingBufferTests.cpp b/tests/audio/src/AudioRingBufferTests.cpp index ea384cad61..7021197792 100644 --- a/tests/audio/src/AudioRingBufferTests.cpp +++ b/tests/audio/src/AudioRingBufferTests.cpp @@ -36,7 +36,7 @@ void AudioRingBufferTests::runAllTests() { int readIndexAt; - AudioRingBuffer ringBuffer(10, false, 10); // makes buffer of 100 int16_t samples + AudioRingBuffer ringBuffer(10, 10); // makes buffer of 100 int16_t samples for (int T = 0; T < 300; T++) { writeIndexAt = 0; From be0ca41eb3206b61b93f7ef62935c8012aed06b8 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 28 Dec 2016 18:27:34 -0800 Subject: [PATCH 54/68] make implicit cast explicit --- libraries/shared/src/GLMHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 6aa4f68f22..ec244553f8 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -370,7 +370,7 @@ glm::quat glmExtractRotation(const glm::mat4& matrix) { glm::vec3 extractScale(const glm::mat4& matrix) { glm::mat3 m(matrix); float det = glm::determinant(m); - if (det < 0) { + if (det < 0.0f) { // left handed matrix, flip sign to compensate. return glm::vec3(-glm::length(m[0]), glm::length(m[1]), glm::length(m[2])); } else { From a72f60152befa0c6ecf4841b106d21b420ea0d5b Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 08:53:37 -0800 Subject: [PATCH 55/68] audit use of QTimer intervals to make sure we're using Qt::PreciseTimer when appropriate --- interface/src/Application.cpp | 12 ++++++------ interface/src/ui/DiskCacheEditor.cpp | 2 +- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 3 ++- libraries/networking/src/DomainHandler.cpp | 4 ++-- libraries/networking/src/LimitedNodeList.cpp | 2 +- libraries/networking/src/NodeList.cpp | 2 +- libraries/networking/src/ThreadedAssignment.cpp | 4 ++-- libraries/script-engine/src/ScriptEngine.cpp | 6 ++++++ libraries/shared/src/SettingManager.cpp | 2 +- plugins/openvr/src/OpenVrHelpers.cpp | 2 +- tests/gpu-test/src/TestWindow.cpp | 1 + tests/networking/src/ResourceTests.cpp | 10 ++++++++-- tests/render-perf/src/main.cpp | 2 +- tests/render-texture-load/src/main.cpp | 2 +- tests/render-utils/src/main.cpp | 2 +- 15 files changed, 35 insertions(+), 21 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d92ba53f6e..835355249a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1135,7 +1135,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&_settingsThread, SIGNAL(finished()), &_settingsTimer, SLOT(stop())); _settingsTimer.moveToThread(&_settingsThread); _settingsTimer.setSingleShot(false); - _settingsTimer.setInterval(SAVE_SETTINGS_INTERVAL); + _settingsTimer.setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable _settingsThread.start(); if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { @@ -1240,7 +1240,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Periodically send fps as a user activity event QTimer* sendStatsTimer = new QTimer(this); - sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); + sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); // 10s, Qt::CoarseTimer acceptable connect(sendStatsTimer, &QTimer::timeout, this, [this]() { QJsonObject properties = {}; @@ -1341,7 +1341,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Periodically check for count of nearby avatars static int lastCountOfNearbyAvatars = -1; QTimer* checkNearbyAvatarsTimer = new QTimer(this); - checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); + checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); // 10 seconds, Qt::CoarseTimer ok connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, [this]() { auto avatarManager = DependencyManager::get(); int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), @@ -1499,16 +1499,16 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000; - _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); + _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize); // Auto-update and close adding asset to world info message box. static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000; - _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); + _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable _addAssetToWorldInfoTimer.setSingleShot(true); connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout); static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000; - _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); + _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); // 8s, Qt::CoarseTimer acceptable _addAssetToWorldErrorTimer.setSingleShot(true); connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout); diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp index ed85b87d63..c7f4bfe39b 100644 --- a/interface/src/ui/DiskCacheEditor.cpp +++ b/interface/src/ui/DiskCacheEditor.cpp @@ -91,7 +91,7 @@ void DiskCacheEditor::makeDialog() { static const int REFRESH_INTERVAL = 100; // msec _refreshTimer = new QTimer(_dialog); - _refreshTimer->setInterval(REFRESH_INTERVAL); + _refreshTimer->setInterval(REFRESH_INTERVAL); // 100ms, Qt::CoarseTimer acceptable _refreshTimer->setSingleShot(false); QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); _refreshTimer->start(); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 3a803172d0..8559872aba 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -426,7 +426,8 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { // a timer with a small interval is used to get better performance. QObject::connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick); QObject::connect(qApp, &QCoreApplication::aboutToQuit, this, &OffscreenQmlSurface::onAboutToQuit); - _updateTimer.setInterval(MIN_TIMER_MS); + _updateTimer.setTimerType(Qt::PreciseTimer); + _updateTimer.setInterval(MIN_TIMER_MS); // 5ms, Qt::PreciseTimer required _updateTimer.start(); auto rootContext = getRootContext(); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index c9106c7162..3c20c3798f 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -41,12 +41,12 @@ DomainHandler::DomainHandler(QObject* parent) : // setup a timeout for failure on settings requests static const int DOMAIN_SETTINGS_TIMEOUT_MS = 5000; - _settingsTimer.setInterval(DOMAIN_SETTINGS_TIMEOUT_MS); + _settingsTimer.setInterval(DOMAIN_SETTINGS_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable connect(&_settingsTimer, &QTimer::timeout, this, &DomainHandler::settingsReceiveFail); // setup the API refresh timer for auto connection information refresh from API when failing to connect const int API_REFRESH_TIMEOUT_MSEC = 2500; - _apiRefreshTimer.setInterval(API_REFRESH_TIMEOUT_MSEC); + _apiRefreshTimer.setInterval(API_REFRESH_TIMEOUT_MSEC); // 2.5s, Qt::CoarseTimer acceptable auto addressManager = DependencyManager::get(); connect(&_apiRefreshTimer, &QTimer::timeout, addressManager.data(), &AddressManager::refreshPreviousLookup); diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a5cf1d9527..b9bf9fea67 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -902,7 +902,7 @@ void LimitedNodeList::startSTUNPublicSocketUpdate() { connect(_initialSTUNTimer.data(), &QTimer::timeout, this, &LimitedNodeList::sendSTUNRequest); const int STUN_INITIAL_UPDATE_INTERVAL_MSECS = 250; - _initialSTUNTimer->setInterval(STUN_INITIAL_UPDATE_INTERVAL_MSECS); + _initialSTUNTimer->setInterval(STUN_INITIAL_UPDATE_INTERVAL_MSECS); // 250ms, Qt::CoarseTimer acceptable // if we don't know the STUN IP yet we need to wait until it is known to start STUN requests if (_stunSockAddr.getAddress().isNull()) { diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 27b3e11fda..0f18dd1b55 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -102,7 +102,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) connect(this, &LimitedNodeList::nodeActivated, this, &NodeList::maybeSendIgnoreSetToNode); // setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect) - _keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); + _keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable connect(&_keepAlivePingTimer, &QTimer::timeout, this, &NodeList::sendKeepAlivePings); connect(&_domainHandler, SIGNAL(connectedToDomain(QString)), &_keepAlivePingTimer, SLOT(start())); connect(&_domainHandler, &DomainHandler::disconnectedFromDomain, &_keepAlivePingTimer, &QTimer::stop); diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index e6e3c17db7..c7b6b05c35 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -27,11 +27,11 @@ ThreadedAssignment::ThreadedAssignment(ReceivedMessage& message) : _statsTimer(this) { static const int STATS_TIMEOUT_MS = 1000; - _statsTimer.setInterval(STATS_TIMEOUT_MS); + _statsTimer.setInterval(STATS_TIMEOUT_MS); // 1s, Qt::CoarseTimer acceptable connect(&_statsTimer, &QTimer::timeout, this, &ThreadedAssignment::sendStatsPacket); connect(&_domainServerTimer, &QTimer::timeout, this, &ThreadedAssignment::checkInWithDomainServerOrExit); - _domainServerTimer.setInterval(DOMAIN_SERVER_CHECK_IN_MSECS); + _domainServerTimer.setInterval(DOMAIN_SERVER_CHECK_IN_MSECS); // 1s, Qt::CoarseTimer acceptable // if the NL tells us we got a DS response, clear our member variable of queued check-ins auto nodeList = DependencyManager::get(); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 6c3f6c7fa8..45d37aa52e 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1087,6 +1087,12 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int QTimer* newTimer = new QTimer(this); newTimer->setSingleShot(isSingleShot); + // The default timer type is not very accurate below about 200ms http://doc.qt.io/qt-5/qt.html#TimerType-enum + static const int MIN_TIMEOUT_FOR_COARSE_TIMER = 200; + if (intervalMS < MIN_TIMEOUT_FOR_COARSE_TIMER) { + newTimer->setTimerType(Qt::PreciseTimer); + } + connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); // make sure the timer stops when the script does diff --git a/libraries/shared/src/SettingManager.cpp b/libraries/shared/src/SettingManager.cpp index 929fe2b0ff..6c246d4cea 100644 --- a/libraries/shared/src/SettingManager.cpp +++ b/libraries/shared/src/SettingManager.cpp @@ -83,7 +83,7 @@ namespace Setting { _saveTimer = new QTimer(this); Q_CHECK_PTR(_saveTimer); _saveTimer->setSingleShot(true); // We will restart it once settings are saved. - _saveTimer->setInterval(SAVE_INTERVAL_MSEC); + _saveTimer->setInterval(SAVE_INTERVAL_MSEC); // 5s, Qt::CoarseTimer acceptable connect(_saveTimer, SIGNAL(timeout()), this, SLOT(saveAll())); } _saveTimer->start(); diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 09713e9f57..29ef640bf3 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -363,7 +363,7 @@ void showMinSpecWarning() { vrOverlay->ShowOverlay(minSpecFailedOverlay); QTimer* timer = new QTimer(&miniApp); - timer->setInterval(FAILED_MIN_SPEC_UPDATE_INTERVAL_MS); + timer->setInterval(FAILED_MIN_SPEC_UPDATE_INTERVAL_MS); // Qt::CoarseTimer acceptable, we don't need this to be frame rate accurate QObject::connect(timer, &QTimer::timeout, [&] { vr::TrackedDevicePose_t vrPoses[vr::k_unMaxTrackedDeviceCount]; vrSystem->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseSeated, 0, vrPoses, vr::k_unMaxTrackedDeviceCount); diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index a9f5216991..0ca1ad2297 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -34,6 +34,7 @@ TestWindow::TestWindow() { auto timer = new QTimer(this); + timer->setTimerType(Qt::PreciseTimer); timer->setInterval(5); connect(timer, &QTimer::timeout, [&] { draw(); }); timer->start(); diff --git a/tests/networking/src/ResourceTests.cpp b/tests/networking/src/ResourceTests.cpp index e6dcf18230..f9ff5887b6 100644 --- a/tests/networking/src/ResourceTests.cpp +++ b/tests/networking/src/ResourceTests.cpp @@ -40,6 +40,12 @@ static QSharedPointer resource; static bool waitForSignal(QObject *sender, const char *signal, int timeout = 1000) { QEventLoop loop; QTimer timer; + + // The default timer type is not very accurate below about 200ms http://doc.qt.io/qt-5/qt.html#TimerType-enum + static const int MIN_TIMEOUT_FOR_COARSE_TIMER = 200; + if (timeout < MIN_TIMEOUT_FOR_COARSE_TIMER) { + timer.setTimerType(Qt::PreciseTimer); + } timer.setInterval(timeout); timer.setSingleShot(true); @@ -61,7 +67,7 @@ void ResourceTests::downloadFirst() { const int timeout = 1000; QEventLoop loop; QTimer timer; - timer.setInterval(timeout); + timer.setInterval(timeout); // 1s, Qt::CoarseTimer acceptable timer.setSingleShot(true); loop.connect(resource, SIGNAL(loaded(QNetworkReply&)), SLOT(quit())); @@ -85,7 +91,7 @@ void ResourceTests::downloadAgain() { const int timeout = 1000; QEventLoop loop; QTimer timer; - timer.setInterval(timeout); + timer.setInterval(timeout); // 1s, Qt::CoarseTimer acceptable timer.setSingleShot(true); loop.connect(resource, SIGNAL(loaded(QNetworkReply&)), SLOT(quit())); diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 31bb13f4d0..df2c552d1a 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -546,7 +546,7 @@ public: restorePosition(); QTimer* timer = new QTimer(this); - timer->setInterval(0); + timer->setInterval(0); // Qt::CoarseTimer acceptable connect(timer, &QTimer::timeout, this, [this] { draw(); }); diff --git a/tests/render-texture-load/src/main.cpp b/tests/render-texture-load/src/main.cpp index 68f007ce70..09a420f018 100644 --- a/tests/render-texture-load/src/main.cpp +++ b/tests/render-texture-load/src/main.cpp @@ -355,7 +355,7 @@ public: } QTimer* timer = new QTimer(this); - timer->setInterval(0); + timer->setInterval(0); // Qt::CoarseTimer acceptable connect(timer, &QTimer::timeout, this, [this] { draw(); }); diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index 7e909bf31f..741fdbdddd 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -201,7 +201,7 @@ int main(int argc, char** argv) { QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; QTimer timer; - timer.setInterval(1); + timer.setInterval(1); // Qt::CoarseTimer acceptable app.connect(&timer, &QTimer::timeout, &app, [&] { window.draw(); }); From f46b8f3e6df4041426b4340b31647f67987462cc Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 29 Dec 2016 10:17:40 -0800 Subject: [PATCH 56/68] Track compositor dropped frames as well. --- plugins/oculus/src/OculusDisplayPlugin.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 7a56124298..bad346b1ca 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -159,6 +159,7 @@ void OculusDisplayPlugin::hmdPresent() { appDroppedFrames = frameStats.AppDroppedFrameCount; } _hardwareStats["app_dropped_frame_count"] = appDroppedFrames; + _hardwareStats["compositor_dropped_frame_count"] = compositorDroppedFrames; } _presentRate.increment(); } From e5901fa875de8701bf1d94abad5fd39d3e982df2 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 29 Dec 2016 11:09:09 -0800 Subject: [PATCH 57/68] Make getHardwareStats thread safe --- plugins/oculus/src/OculusDisplayPlugin.cpp | 16 +++++++++++++--- plugins/oculus/src/OculusDisplayPlugin.h | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index bad346b1ca..060823a748 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -22,6 +22,13 @@ const char* OculusDisplayPlugin::NAME { "Oculus Rift" }; static ovrPerfHudMode currentDebugMode = ovrPerfHud_Off; + +OculusDisplayPlugin::OculusDisplayPlugin() { + _appDroppedFrames.store(0); + _compositorDroppedFrames.store(0); +} + + bool OculusDisplayPlugin::internalActivate() { bool result = Parent::internalActivate(); currentDebugMode = ovrPerfHud_Off; @@ -158,15 +165,18 @@ void OculusDisplayPlugin::hmdPresent() { compositorDroppedFrames = frameStats.CompositorDroppedFrameCount; appDroppedFrames = frameStats.AppDroppedFrameCount; } - _hardwareStats["app_dropped_frame_count"] = appDroppedFrames; - _hardwareStats["compositor_dropped_frame_count"] = compositorDroppedFrames; + _appDroppedFrames.store(appDroppedFrames); + _compositorDroppedFrames.store(compositorDroppedFrames); } _presentRate.increment(); } QJsonObject OculusDisplayPlugin::getHardwareStats() const { - return _hardwareStats; + QJsonObject hardwareStats; + hardwareStats["app_dropped_frame_count"] = _appDroppedFrames.load(); + hardwareStats["compositor_dropped_frame_count"] = _compositorDroppedFrames.load(); + return hardwareStats; } bool OculusDisplayPlugin::isHmdMounted() const { diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index a953e01e2f..e44596d6e9 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -12,6 +12,7 @@ class OculusDisplayPlugin : public OculusBaseDisplayPlugin { using Parent = OculusBaseDisplayPlugin; public: + OculusDisplayPlugin(); ~OculusDisplayPlugin(); const QString getName() const override { return NAME; } @@ -36,6 +37,7 @@ private: gpu::FramebufferPointer _outputFramebuffer; bool _customized { false }; - QJsonObject _hardwareStats; + std::atomic_int _compositorDroppedFrames; + std::atomic_int _appDroppedFrames; }; From d00ae05e26a5b689db828117c38e90cbdef4862b Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 28 Dec 2016 17:45:05 -0500 Subject: [PATCH 58/68] add background to forward renderer --- .../render-utils/src/RenderForwardTask.cpp | 28 +++++++++++++++++++ .../render-utils/src/RenderForwardTask.h | 10 ++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 4e971c6677..b8560ef2e8 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -80,6 +80,9 @@ RenderForwardTask::RenderForwardTask(CullFunctor cullFunctor) { const auto framebuffer = addJob("PrepareFramebuffer"); + addJob("DrawBackground", background); + + // bounds do not draw on stencil buffer, so they must come last addJob("DrawBounds", opaques); // Blit! @@ -173,6 +176,8 @@ void DrawBounds::run(const SceneContextPointer& sceneContext, const RenderContex RenderArgs* args = renderContext->args; gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + // Setup projection glm::mat4 projMat; Transform viewMat; @@ -197,3 +202,26 @@ void DrawBounds::run(const SceneContextPointer& sceneContext, const RenderContex } }); } + +void DrawBackground::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const Inputs& items) { + RenderArgs* args = renderContext->args; + + gpu::doInBatch(args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + + batch.enableSkybox(true); + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + + // Setup projection + glm::mat4 projMat; + Transform viewMat; + args->getViewFrustum().evalProjectionMatrix(projMat); + args->getViewFrustum().evalViewTransform(viewMat); + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewMat); + + renderItems(sceneContext, renderContext, items); + }); + args->_batch = nullptr; +} diff --git a/libraries/render-utils/src/RenderForwardTask.h b/libraries/render-utils/src/RenderForwardTask.h index a4839e18ec..90d2ceb79c 100755 --- a/libraries/render-utils/src/RenderForwardTask.h +++ b/libraries/render-utils/src/RenderForwardTask.h @@ -53,4 +53,12 @@ private: int _scaleLocation { -1 }; }; -#endif // hifi_RenderForwardTask_h \ No newline at end of file +class DrawBackground { +public: + using Inputs = render::ItemBounds; + using JobModel = render::Job::ModelI; + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Inputs& background); +}; + +#endif // hifi_RenderForwardTask_h From c75dd236f2fad0334c71622bbe48086ecb5e0f00 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 29 Dec 2016 19:11:06 -0500 Subject: [PATCH 59/68] rm unused forward pipelines --- libraries/render-utils/src/RenderForwardTask.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index b8560ef2e8..ed4f3d27e3 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -31,14 +31,8 @@ #include using namespace render; -extern void initOverlay3DPipelines(render::ShapePlumber& plumber); -extern void initDeferredPipelines(render::ShapePlumber& plumber); RenderForwardTask::RenderForwardTask(CullFunctor cullFunctor) { - // Prepare the ShapePipelines - ShapePlumberPointer shapePlumber = std::make_shared(); - initDeferredPipelines(*shapePlumber); - // CPU jobs: // Fetch and cull the items from the scene const auto spatialSelection = addJob("FetchSceneSelection"); From 0c22fcb5c42896985e81de4816cfd7f4528e293f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 16:41:14 -0800 Subject: [PATCH 60/68] better handling of timers --- interface/src/ui/DiskCacheEditor.cpp | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 89 +++++++++++++++----- libraries/script-engine/src/ScriptEngine.h | 3 + 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp index c7f4bfe39b..1a7be8642b 100644 --- a/interface/src/ui/DiskCacheEditor.cpp +++ b/interface/src/ui/DiskCacheEditor.cpp @@ -91,7 +91,7 @@ void DiskCacheEditor::makeDialog() { static const int REFRESH_INTERVAL = 100; // msec _refreshTimer = new QTimer(_dialog); - _refreshTimer->setInterval(REFRESH_INTERVAL); // 100ms, Qt::CoarseTimer acceptable + _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy _refreshTimer->setSingleShot(false); QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); _refreshTimer->start(); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 45d37aa52e..fed1c6ffa4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -800,7 +800,7 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& _registeredHandlers[entityID] = RegisteredEventHandlers(); } CallbackList& handlersForEvent = _registeredHandlers[entityID][eventName]; - CallbackData handlerData = {handler, currentEntityIdentifier, currentSandboxURL}; + CallbackData handlerData = { handler, currentEntityIdentifier, currentSandboxURL }; handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } @@ -840,6 +840,15 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi } void ScriptEngine::run() { + +#ifdef _WIN32 + // VS13 does not sleep_until unless it uses the system_clock, see: + // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ + using clock = std::chrono::system_clock; +#else + using clock = std::chrono::high_resolution_clock; +#endif + if (DependencyManager::get()->isStopped()) { return; // bail early - avoid setting state in init(), as evaluate() will bail too } @@ -853,14 +862,6 @@ void ScriptEngine::run() { QScriptValue result = evaluate(_scriptContents, _fileNameString); -#ifdef _WIN32 - // VS13 does not sleep_until unless it uses the system_clock, see: - // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ - using clock = std::chrono::system_clock; -#else - using clock = std::chrono::high_resolution_clock; -#endif - clock::time_point startTime = clock::now(); int thisFrame = 0; @@ -878,23 +879,48 @@ void ScriptEngine::run() { // Throttle to SCRIPT_FPS // We'd like to try to keep the script at a solid SCRIPT_FPS update rate. And so we will // calculate a sleepUntil to be the time from our start time until the original target - // sleepUntil for this frame. - const std::chrono::microseconds FRAME_DURATION(USECS_PER_SECOND / SCRIPT_FPS + 1); - clock::time_point targetSleepUntil(startTime + thisFrame++ * FRAME_DURATION); + // sleepUntil for this frame. This approach will allow us to "catch up" in the event + // that some of our script udpates/frames take a little bit longer than the target average + // to execute. + // NOTE: if we go to variable SCRIPT_FPS, then we will need to reconsider this approach + const std::chrono::microseconds TARGET_SCRIPT_FRAME_DURATION(USECS_PER_SECOND / SCRIPT_FPS + 1); + clock::time_point targetSleepUntil(startTime + (thisFrame++ * TARGET_SCRIPT_FRAME_DURATION)); - // However, if our sleepUntil is not at least our average update time into the future - // it means our script is taking too long in it's updates, and we want to punish the - // script a little bit. So we will force the sleepUntil to be at least our averageUpdate - // time into the future. + // However, if our sleepUntil is not at least our average update and timer execution time + // into the future it means our script is taking too long in its updates, and we want to + // punish the script a little bit. So we will force the sleepUntil to be at least our + // averageUpdate + averageTimerPerFrame time into the future. auto averageUpdate = totalUpdates / thisFrame; - auto sleepUntil = std::max(targetSleepUntil, beforeSleep + averageUpdate); + auto averageTimerPerFrame = _totalTimerExecution / thisFrame; + auto averageTimerAndUpdate = averageUpdate + averageTimerPerFrame; + auto sleepUntil = std::max(targetSleepUntil, beforeSleep + averageTimerAndUpdate); // We don't want to actually sleep for too long, because it causes our scripts to hang // on shutdown and stop... so we want to loop and sleep until we've spent our time in // purgatory, constantly checking to see if our script was asked to end + bool processedEvents = false; while (!_isFinished && clock::now() < sleepUntil) { + QCoreApplication::processEvents(); // before we sleep again, give events a chance to process - auto thisSleepUntil = std::min(sleepUntil, clock::now() + FRAME_DURATION); + processedEvents = true; + + // If after processing events, we're past due, exit asap + if (clock::now() >= sleepUntil) { + break; + } + + // determine how long before the next timer should fire, we'd ideally like to sleep just + // that long, so the next processEvents() will allow the timers to fire on time. + const std::chrono::microseconds minTimerTimeRemaining(USECS_PER_MSEC * getTimersRemainingTime()); + + // However, if we haven't yet slept at least as long as our average timer per frame, then we will + // punish the timers to at least wait as long as the average run time of the timers. + auto untilTimer = std::max(minTimerTimeRemaining, averageTimerPerFrame); + + // choose the closest time point, our + auto remainingSleepUntil = std::chrono::duration_cast(sleepUntil - clock::now()); + auto closestUntil = std::min(remainingSleepUntil, untilTimer); + auto thisSleepUntil = std::min(sleepUntil, clock::now() + closestUntil); std::this_thread::sleep_until(thisSleepUntil); } @@ -919,7 +945,10 @@ void ScriptEngine::run() { break; } - QCoreApplication::processEvents(); + // Only call this if we didn't processEvents as part of waiting for next frame + if (!processedEvents) { + QCoreApplication::processEvents(); + } if (_isFinished) { break; @@ -982,6 +1011,21 @@ void ScriptEngine::run() { emit doneRunning(); } +quint64 ScriptEngine::getTimersRemainingTime() { + quint64 minimumTime = USECS_PER_SECOND; // anything larger than this can be ignored + QMutableHashIterator i(_timerFunctionMap); + while (i.hasNext()) { + i.next(); + QTimer* timer = i.key(); + int remainingTime = timer->remainingTime(); + if (remainingTime >= 0) { + minimumTime = std::min((quint64)remainingTime, minimumTime); + } + } + return minimumTime; +} + + // NOTE: This is private because it must be called on the same thread that created the timers, which is why // we want to only call it in our own run "shutdown" processing. void ScriptEngine::stopAllTimers() { @@ -1077,7 +1121,12 @@ void ScriptEngine::timerFired() { // call the associated JS function, if it exists if (timerData.function.isValid()) { + auto preTimer = p_high_resolution_clock::now(); callWithEnvironment(timerData.definingEntityIdentifier, timerData.definingSandboxURL, timerData.function, timerData.function, QScriptValueList()); + auto postTimer = p_high_resolution_clock::now(); + auto elapsed = (postTimer - preTimer); + _totalTimerExecution += std::chrono::duration_cast(elapsed); + } } @@ -1098,7 +1147,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int // make sure the timer stops when the script does connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); - CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL}; + CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL }; _timerFunctionMap.insert(newTimer, timerData); newTimer->start(intervalMS); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 1606e70d28..803aa7fa22 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -218,6 +218,7 @@ protected: void init(); bool evaluatePending() const { return _evaluatesPending > 0; } + quint64 getTimersRemainingTime(); void timerFired(); void stopAllTimers(); void stopAllTimersForEntityScript(const EntityItemID& entityID); @@ -252,6 +253,8 @@ protected: std::function _emitScriptUpdates{ [](){ return true; } }; std::recursive_mutex _lock; + + std::chrono::microseconds _totalTimerExecution { 0 }; }; #endif // hifi_ScriptEngine_h From 4b9345d7c6eda6358dc98923a3420280cdf7f88e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 16:45:16 -0800 Subject: [PATCH 61/68] cleanup accidental change --- libraries/script-engine/src/ScriptEngine.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index fed1c6ffa4..03b01e1b9c 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -840,15 +840,6 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi } void ScriptEngine::run() { - -#ifdef _WIN32 - // VS13 does not sleep_until unless it uses the system_clock, see: - // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ - using clock = std::chrono::system_clock; -#else - using clock = std::chrono::high_resolution_clock; -#endif - if (DependencyManager::get()->isStopped()) { return; // bail early - avoid setting state in init(), as evaluate() will bail too } @@ -862,6 +853,14 @@ void ScriptEngine::run() { QScriptValue result = evaluate(_scriptContents, _fileNameString); +#ifdef _WIN32 + // VS13 does not sleep_until unless it uses the system_clock, see: + // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ + using clock = std::chrono::system_clock; +#else + using clock = std::chrono::high_resolution_clock; +#endif + clock::time_point startTime = clock::now(); int thisFrame = 0; From 6c86675f24fa5c46badbafe6e9aa854b1bc9e4c6 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 18:36:32 -0800 Subject: [PATCH 62/68] tweaks to bow script to debug judder --- unpublishedScripts/marketplace/bow/bow.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unpublishedScripts/marketplace/bow/bow.js b/unpublishedScripts/marketplace/bow/bow.js index 818960e335..61c52a6103 100644 --- a/unpublishedScripts/marketplace/bow/bow.js +++ b/unpublishedScripts/marketplace/bow/bow.js @@ -154,6 +154,7 @@ }, continueEquip: function(entityID, args) { this.deltaTime = checkInterval(); + print("continueEquip deltaTime:" + this.deltaTime); //debounce during debugging -- maybe we're updating too fast? if (USE_DEBOUNCE === true) { this.sinceLastUpdate = this.sinceLastUpdate + this.deltaTime; @@ -374,8 +375,8 @@ this.pullBackDistance = 0; this.state = STATE_ARROW_GRABBED; } else { - this.updateString(); this.updateArrowPositionInNotch(false, false); + this.updateString(); } } if (this.state === STATE_ARROW_GRABBED) { From 73cb100d7690bc915760f2e73a72ea1984077cb3 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 19:00:06 -0800 Subject: [PATCH 63/68] more debugging of bow --- unpublishedScripts/marketplace/bow/bow.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unpublishedScripts/marketplace/bow/bow.js b/unpublishedScripts/marketplace/bow/bow.js index 61c52a6103..183703ba91 100644 --- a/unpublishedScripts/marketplace/bow/bow.js +++ b/unpublishedScripts/marketplace/bow/bow.js @@ -593,3 +593,8 @@ return bow; }); + +var TIMER_INTERVAL = 5; +var RPCkicker = Script.setInterval(function() { + // do nothing, but make sure we get RPC messages in a reliable way +},TIMER_INTERVAL); From 1bf39652235f84b28fcb008671fcb54e9e59934b Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 19:03:29 -0800 Subject: [PATCH 64/68] more debugging of bow --- unpublishedScripts/marketplace/bow/bow.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unpublishedScripts/marketplace/bow/bow.js b/unpublishedScripts/marketplace/bow/bow.js index 183703ba91..6850ca48c6 100644 --- a/unpublishedScripts/marketplace/bow/bow.js +++ b/unpublishedScripts/marketplace/bow/bow.js @@ -595,6 +595,11 @@ }); var TIMER_INTERVAL = 5; +var lastRPC = Date.now(); var RPCkicker = Script.setInterval(function() { // do nothing, but make sure we get RPC messages in a reliable way + var now = Date.now(); + deltaTime = now - lastRPC; + lastRPC = now; + print("RPCkicker interval:"+deltaTime); },TIMER_INTERVAL); From 65c8d4b16b5513f513c9a61da6b1fa22829e9e48 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 19:16:55 -0800 Subject: [PATCH 65/68] more debugging of bow --- unpublishedScripts/marketplace/bow/bow.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/unpublishedScripts/marketplace/bow/bow.js b/unpublishedScripts/marketplace/bow/bow.js index 6850ca48c6..d42f1c223d 100644 --- a/unpublishedScripts/marketplace/bow/bow.js +++ b/unpublishedScripts/marketplace/bow/bow.js @@ -591,15 +591,16 @@ Messages.subscribe('Hifi-Object-Manipulation'); Messages.messageReceived.connect(bow.handleMessages); - return bow; -}); + var TIMER_INTERVAL = 5; + var lastRPC = Date.now(); + var RPCkicker = Script.setInterval(function() { + // do nothing, but make sure we get RPC messages in a reliable way + var now = Date.now(); + deltaTime = now - lastRPC; + lastRPC = now; + print("RPCkicker interval:"+deltaTime); + },TIMER_INTERVAL); -var TIMER_INTERVAL = 5; -var lastRPC = Date.now(); -var RPCkicker = Script.setInterval(function() { - // do nothing, but make sure we get RPC messages in a reliable way - var now = Date.now(); - deltaTime = now - lastRPC; - lastRPC = now; - print("RPCkicker interval:"+deltaTime); -},TIMER_INTERVAL); + + return bow; +}); \ No newline at end of file From b1d556aee2efd01a2b4815d49dfd079d68e0e6d8 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 29 Dec 2016 19:21:18 -0800 Subject: [PATCH 66/68] more debugging of bow --- unpublishedScripts/marketplace/bow/bow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unpublishedScripts/marketplace/bow/bow.js b/unpublishedScripts/marketplace/bow/bow.js index d42f1c223d..5afeb99ce4 100644 --- a/unpublishedScripts/marketplace/bow/bow.js +++ b/unpublishedScripts/marketplace/bow/bow.js @@ -598,7 +598,7 @@ var now = Date.now(); deltaTime = now - lastRPC; lastRPC = now; - print("RPCkicker interval:"+deltaTime); + //print("RPCkicker interval:"+deltaTime); },TIMER_INTERVAL); From 1fcfc9444b200d0707fceed4152c31af1473077e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 30 Dec 2016 08:12:44 -0800 Subject: [PATCH 67/68] revert bow.js changes --- unpublishedScripts/marketplace/bow/bow.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/unpublishedScripts/marketplace/bow/bow.js b/unpublishedScripts/marketplace/bow/bow.js index 5afeb99ce4..818960e335 100644 --- a/unpublishedScripts/marketplace/bow/bow.js +++ b/unpublishedScripts/marketplace/bow/bow.js @@ -154,7 +154,6 @@ }, continueEquip: function(entityID, args) { this.deltaTime = checkInterval(); - print("continueEquip deltaTime:" + this.deltaTime); //debounce during debugging -- maybe we're updating too fast? if (USE_DEBOUNCE === true) { this.sinceLastUpdate = this.sinceLastUpdate + this.deltaTime; @@ -375,8 +374,8 @@ this.pullBackDistance = 0; this.state = STATE_ARROW_GRABBED; } else { - this.updateArrowPositionInNotch(false, false); this.updateString(); + this.updateArrowPositionInNotch(false, false); } } if (this.state === STATE_ARROW_GRABBED) { @@ -591,16 +590,5 @@ Messages.subscribe('Hifi-Object-Manipulation'); Messages.messageReceived.connect(bow.handleMessages); - var TIMER_INTERVAL = 5; - var lastRPC = Date.now(); - var RPCkicker = Script.setInterval(function() { - // do nothing, but make sure we get RPC messages in a reliable way - var now = Date.now(); - deltaTime = now - lastRPC; - lastRPC = now; - //print("RPCkicker interval:"+deltaTime); - },TIMER_INTERVAL); - - return bow; -}); \ No newline at end of file +}); From a42ac89a6d83c909a45b75b31361c65243563836 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Fri, 30 Dec 2016 13:47:31 -0800 Subject: [PATCH 68/68] Faster HRTF New AVX2 implementation of FIR_1x4() achieves 24 FLOPS per clock on HSW/BDW/SKL Removed old AVX implementation 32-byte alignment of SIMD buffers and tables Fixed the poorly optimized code produced by GCC -O2 --- libraries/audio/src/AudioHRTF.cpp | 62 +++++++------ libraries/audio/src/AudioHRTFData.h | 58 +++++++------ libraries/audio/src/avx/AudioHRTF_avx.cpp | 96 --------------------- libraries/audio/src/avx2/AudioHRTF_avx2.cpp | 94 ++++++++++++++++++++ 4 files changed, 164 insertions(+), 146 deletions(-) delete mode 100644 libraries/audio/src/avx/AudioHRTF_avx.cpp create mode 100644 libraries/audio/src/avx2/AudioHRTF_avx2.cpp diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index 5984187203..84e3622498 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -16,6 +16,14 @@ #include "AudioHRTF.h" #include "AudioHRTFData.h" +#if defined(_MSC_VER) +#define ALIGN32 __declspec(align(32)) +#elif defined(__GNUC__) +#define ALIGN32 __attribute__((aligned(32))) +#else +#define ALIGN32 +#endif + #ifndef MAX #define MAX(a,b) (((a) > (b)) ? (a) : (b)) #endif @@ -30,7 +38,7 @@ // Transients in the time-varying Thiran allpass filter are eliminated by the initial delay. // Valimaki, Laakso. "Elimination of Transients in Time-Varying Allpass Fractional Delay Filters" // -static const float crossfadeTable[HRTF_BLOCK] = { +ALIGN32 static const float crossfadeTable[HRTF_BLOCK] = { 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 1.0000000000f, 0.9999545513f, 0.9998182135f, 0.9995910114f, 0.9992729863f, 0.9988641959f, 0.9983647147f, 0.9977746334f, 0.9970940592f, 0.9963231160f, 0.9954619438f, 0.9945106993f, 0.9934695553f, 0.9923387012f, 0.9911183425f, 0.9898087010f, 0.9884100149f, @@ -192,25 +200,29 @@ static void FIR_1x4_SSE(float* src, float* dst0, float* dst1, float* dst2, float for (int k = 0; k < HRTF_TAPS; k += 4) { - acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-0]), _mm_loadu_ps(&ps[k+0]))); - acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-0]), _mm_loadu_ps(&ps[k+0]))); - acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-0]), _mm_loadu_ps(&ps[k+0]))); - acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-0]), _mm_loadu_ps(&ps[k+0]))); + __m128 x0 = _mm_loadu_ps(&ps[k+0]); + acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-0]), x0)); + acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-0]), x0)); + acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-0]), x0)); + acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-0]), x0)); - acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-1]), _mm_loadu_ps(&ps[k+1]))); - acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-1]), _mm_loadu_ps(&ps[k+1]))); - acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-1]), _mm_loadu_ps(&ps[k+1]))); - acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-1]), _mm_loadu_ps(&ps[k+1]))); + __m128 x1 = _mm_loadu_ps(&ps[k+1]); + acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-1]), x1)); + acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-1]), x1)); + acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-1]), x1)); + acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-1]), x1)); - acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-2]), _mm_loadu_ps(&ps[k+2]))); - acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-2]), _mm_loadu_ps(&ps[k+2]))); - acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-2]), _mm_loadu_ps(&ps[k+2]))); - acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-2]), _mm_loadu_ps(&ps[k+2]))); + __m128 x2 = _mm_loadu_ps(&ps[k+2]); + acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-2]), x2)); + acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-2]), x2)); + acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-2]), x2)); + acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-2]), x2)); - acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-3]), _mm_loadu_ps(&ps[k+3]))); - acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-3]), _mm_loadu_ps(&ps[k+3]))); - acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-3]), _mm_loadu_ps(&ps[k+3]))); - acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-3]), _mm_loadu_ps(&ps[k+3]))); + __m128 x3 = _mm_loadu_ps(&ps[k+3]); + acc0 = _mm_add_ps(acc0, _mm_mul_ps(_mm_load1_ps(&coef0[-k-3]), x3)); + acc1 = _mm_add_ps(acc1, _mm_mul_ps(_mm_load1_ps(&coef1[-k-3]), x3)); + acc2 = _mm_add_ps(acc2, _mm_mul_ps(_mm_load1_ps(&coef2[-k-3]), x3)); + acc3 = _mm_add_ps(acc3, _mm_mul_ps(_mm_load1_ps(&coef3[-k-3]), x3)); } _mm_storeu_ps(&dst0[i], acc0); @@ -226,11 +238,11 @@ static void FIR_1x4_SSE(float* src, float* dst0, float* dst1, float* dst2, float #include "CPUDetect.h" -void FIR_1x4_AVX(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames); +void FIR_1x4_AVX2(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames); static void FIR_1x4(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames) { - static auto f = cpuSupportsAVX() ? FIR_1x4_AVX : FIR_1x4_SSE; + static auto f = cpuSupportsAVX2() ? FIR_1x4_AVX2 : FIR_1x4_SSE; (*f)(src, dst0, dst1, dst2, dst3, coef, numFrames); // dispatch } @@ -842,12 +854,12 @@ void AudioHRTF::render(int16_t* input, float* output, int index, float azimuth, assert(index < HRTF_TABLES); assert(numFrames == HRTF_BLOCK); - float in[HRTF_TAPS + HRTF_BLOCK]; // mono - float firCoef[4][HRTF_TAPS]; // 4-channel - float firBuffer[4][HRTF_DELAY + HRTF_BLOCK]; // 4-channel - float bqCoef[5][8]; // 4-channel (interleaved) - float bqBuffer[4 * HRTF_BLOCK]; // 4-channel (interleaved) - int delay[4]; // 4-channel (interleaved) + ALIGN32 float in[HRTF_TAPS + HRTF_BLOCK]; // mono + ALIGN32 float firCoef[4][HRTF_TAPS]; // 4-channel + ALIGN32 float firBuffer[4][HRTF_DELAY + HRTF_BLOCK]; // 4-channel + ALIGN32 float bqCoef[5][8]; // 4-channel (interleaved) + ALIGN32 float bqBuffer[4 * HRTF_BLOCK]; // 4-channel (interleaved) + int delay[4]; // 4-channel (interleaved) // to avoid polluting the cache, old filters are recomputed instead of stored setFilters(firCoef, bqCoef, delay, index, _azimuthState, _distanceState, _gainState, L0); diff --git a/libraries/audio/src/AudioHRTFData.h b/libraries/audio/src/AudioHRTFData.h index e317dee7c7..b2d3ebf847 100644 --- a/libraries/audio/src/AudioHRTFData.h +++ b/libraries/audio/src/AudioHRTFData.h @@ -30,6 +30,14 @@ // 6) Truncate filter length to 2.5ms using rectangular window with 8-tap Hanning taper // +#if defined(_MSC_VER) +#define ALIGN32 __declspec(align(32)) +#elif defined(__GNUC__) +#define ALIGN32 __attribute__((aligned(32))) +#else +#define ALIGN32 +#endif + static const float itd_1002_table[HRTF_AZIMUTHS] = { -0.07851f, 0.85414f, 1.77170f, 2.71137f, 3.71065f, 4.74907f, 5.79892f, 6.82396f, 7.82837f, 8.80796f, 9.75426f, 10.68332f, 11.59979f, 12.48520f, 13.36135f, 14.19234f, @@ -42,7 +50,7 @@ static const float itd_1002_table[HRTF_AZIMUTHS] = { -8.39670f, -7.23606f, -6.09663f, -5.05593f, -4.06186f, -3.07465f, -2.06122f, -1.05417f, }; -static const float ir_1002_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1002_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 8.341559e-01f, 1.886116e-02f, 2.677664e-01f, -7.037183e-02f, -4.147236e-02f, -2.761588e-01f, 2.310035e-01f, -1.643133e-01f, @@ -1497,7 +1505,7 @@ static const float itd_1003_table[HRTF_AZIMUTHS] = { -6.64380f, -5.73462f, -4.83364f, -3.97025f, -3.08925f, -2.16621f, -1.19364f, -0.20709f, }; -static const float ir_1003_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1003_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 9.266240e-01f, 1.260510e-01f, 5.051008e-02f, -3.536678e-01f, 2.462246e-02f, 4.465557e-02f, 6.813228e-02f, -6.063477e-02f, @@ -2952,7 +2960,7 @@ static const float itd_1004_table[HRTF_AZIMUTHS] = { -7.55720f, -6.55578f, -5.59246f, -4.69657f, -3.80733f, -2.88567f, -1.90337f, -0.89923f, }; -static const float ir_1004_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1004_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.326633e-01f, 4.279429e-01f, -5.910516e-02f, -2.480760e-01f, -9.903029e-02f, 9.215562e-02f, -2.893536e-02f, 5.464364e-02f, @@ -4407,7 +4415,7 @@ static const float itd_1005_table[HRTF_AZIMUTHS] = { -6.80079f, -6.03878f, -5.25100f, -4.34973f, -3.39268f, -2.41226f, -1.45444f, -0.50375f, }; -static const float ir_1005_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1005_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 8.515557e-01f, 1.208618e-01f, 3.238278e-01f, -3.605847e-01f, -3.354420e-02f, -1.829174e-01f, 2.309960e-01f, -1.744711e-01f, @@ -5862,7 +5870,7 @@ static const float itd_1007_table[HRTF_AZIMUTHS] = { -7.68135f, -6.69801f, -5.72186f, -4.72708f, -3.74413f, -2.77373f, -1.79032f, -0.81823f, }; -static const float ir_1007_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1007_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 6.544936e-01f, 2.820574e-01f, 1.850652e-01f, -2.597811e-01f, -5.585250e-02f, -7.975905e-02f, 8.143960e-02f, -5.044548e-02f, @@ -7317,7 +7325,7 @@ static const float itd_1012_table[HRTF_AZIMUTHS] = { -7.32159f, -6.30684f, -5.31969f, -4.40260f, -3.50567f, -2.60925f, -1.70893f, -0.80401f, }; -static const float ir_1012_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1012_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 8.505165e-01f, 9.074762e-02f, 3.296598e-01f, -5.213905e-01f, 1.348379e-01f, -1.828924e-01f, 1.400077e-01f, -4.071996e-02f, @@ -8772,7 +8780,7 @@ static const float itd_1014_table[HRTF_AZIMUTHS] = { -7.51312f, -6.52705f, -5.56262f, -4.72113f, -3.90664f, -3.07768f, -2.22719f, -1.37514f, }; -static const float ir_1014_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1014_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 6.542071e-01f, 4.575563e-01f, 1.118072e-02f, -1.823464e-01f, -2.222339e-01f, 1.371357e-01f, 7.027919e-03f, -5.534852e-02f, @@ -10227,7 +10235,7 @@ static const float itd_1017_table[HRTF_AZIMUTHS] = { -7.46925f, -6.49073f, -5.52501f, -4.62178f, -3.74041f, -2.86207f, -1.97362f, -1.07512f, }; -static const float ir_1017_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1017_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.470867e-01f, 2.686078e-01f, 2.097923e-01f, -2.935018e-01f, -8.687224e-02f, -4.547367e-02f, 6.920631e-03f, 3.752071e-02f, @@ -11682,7 +11690,7 @@ static const float itd_1020_table[HRTF_AZIMUTHS] = { -8.28071f, -7.36311f, -6.43732f, -5.49298f, -4.53728f, -3.57601f, -2.59830f, -1.63297f, }; -static const float ir_1020_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1020_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 6.953847e-01f, 3.081256e-01f, 2.474324e-01f, -3.025226e-01f, -1.119181e-01f, -4.966299e-02f, 5.727889e-02f, 6.715016e-03f, @@ -13137,7 +13145,7 @@ static const float itd_1021_table[HRTF_AZIMUTHS] = { -8.12772f, -7.17689f, -6.23068f, -5.27554f, -4.32391f, -3.38489f, -2.46445f, -1.54407f, }; -static const float ir_1021_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1021_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.807186e-01f, 3.835520e-01f, 1.208801e-01f, -4.044311e-01f, -5.188029e-02f, -7.750225e-02f, 1.739668e-01f, -6.599168e-02f, @@ -14592,7 +14600,7 @@ static const float itd_1022_table[HRTF_AZIMUTHS] = { -7.19675f, -6.30334f, -5.39609f, -4.47018f, -3.53964f, -2.62393f, -1.75389f, -0.90222f, }; -static const float ir_1022_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1022_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.053226e-01f, 2.645844e-01f, 2.462055e-01f, -2.145682e-01f, -1.333283e-01f, -1.751403e-01f, 2.721890e-01f, -1.743790e-01f, @@ -16047,7 +16055,7 @@ static const float itd_1026_table[HRTF_AZIMUTHS] = { -7.45209f, -6.46598f, -5.49746f, -4.54220f, -3.60610f, -2.68084f, -1.74087f, -0.80841f, }; -static const float ir_1026_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1026_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.150396e-01f, 3.144234e-01f, 9.132840e-02f, -2.128668e-01f, -1.899010e-01f, 1.362356e-01f, -4.105226e-02f, 4.896281e-02f, @@ -17502,7 +17510,7 @@ static const float itd_1028_table[HRTF_AZIMUTHS] = { -7.80099f, -6.89255f, -5.95721f, -5.04107f, -4.11968f, -3.20233f, -2.33316f, -1.46289f, }; -static const float ir_1028_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1028_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 9.491360e-01f, 2.952796e-01f, -1.585342e-01f, -3.497386e-01f, 1.204260e-01f, -4.886012e-02f, 5.238760e-02f, -8.209077e-03f, @@ -18957,7 +18965,7 @@ static const float itd_1038_table[HRTF_AZIMUTHS] = { -6.69661f, -5.65906f, -4.62851f, -3.63493f, -2.66802f, -1.71997f, -0.76853f, 0.18497f, }; -static const float ir_1038_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1038_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 9.325991e-01f, 1.817283e-01f, 5.397613e-02f, -4.121773e-01f, -7.921759e-03f, -4.009945e-02f, 1.499187e-01f, -1.838252e-02f, @@ -20412,7 +20420,7 @@ static const float itd_1041_table[HRTF_AZIMUTHS] = { -7.03257f, -6.07458f, -5.13664f, -4.24453f, -3.37177f, -2.49083f, -1.55807f, -0.62014f, }; -static const float ir_1041_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1041_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.012368e-01f, 2.006662e-01f, 3.173636e-01f, -2.865733e-01f, 1.345042e-01f, -5.030394e-01f, 3.717757e-01f, -1.138039e-01f, @@ -21867,7 +21875,7 @@ static const float itd_1042_table[HRTF_AZIMUTHS] = { -7.79822f, -6.84403f, -5.88862f, -4.94525f, -3.99704f, -3.03547f, -2.06207f, -1.07916f, }; -static const float ir_1042_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1042_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 9.114429e-01f, 2.201994e-03f, 3.703525e-01f, -4.825957e-01f, 1.210277e-01f, -2.471091e-01f, 1.766662e-01f, -5.840113e-03f, @@ -23322,7 +23330,7 @@ static const float itd_1043_table[HRTF_AZIMUTHS] = { -6.81973f, -5.86664f, -4.92096f, -3.99232f, -3.07973f, -2.16321f, -1.20142f, -0.22538f, }; -static const float ir_1043_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1043_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.339447e-01f, 1.339343e-01f, 4.031645e-01f, -4.891909e-01f, 8.751389e-02f, -2.110783e-01f, 2.573841e-01f, -1.050324e-01f, @@ -24777,7 +24785,7 @@ static const float itd_1044_table[HRTF_AZIMUTHS] = { -7.31965f, -6.37963f, -5.45379f, -4.54748f, -3.59370f, -2.59525f, -1.67705f, -0.73882f, }; -static const float ir_1044_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1044_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.028871e-01f, 2.381998e-01f, 4.686725e-01f, -5.412304e-01f, 1.262568e-01f, -3.198619e-01f, 1.963468e-01f, -4.016186e-02f, @@ -26232,7 +26240,7 @@ static const float itd_1047_table[HRTF_AZIMUTHS] = { -9.01225f, -7.93667f, -6.85884f, -5.78919f, -4.72064f, -3.66640f, -2.66295f, -1.65780f, }; -static const float ir_1047_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1047_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 7.788578e-01f, 1.598904e-01f, 2.366520e-01f, -3.524184e-01f, -8.784474e-03f, -5.144472e-02f, 8.679429e-02f, -1.634258e-02f, @@ -27687,7 +27695,7 @@ static const float itd_1048_table[HRTF_AZIMUTHS] = { -7.15985f, -6.30472f, -5.41513f, -4.54994f, -3.62385f, -2.66142f, -1.79111f, -0.94033f, }; -static const float ir_1048_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1048_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 8.865287e-01f, 2.972076e-01f, -1.305391e-01f, -1.213860e-01f, -1.948535e-01f, 1.458427e-01f, -8.912857e-02f, 9.493978e-02f, @@ -29142,7 +29150,7 @@ static const float itd_1050_table[HRTF_AZIMUTHS] = { -6.52690f, -5.58085f, -4.64474f, -3.71658f, -2.80444f, -1.92096f, -1.07543f, -0.23450f, }; -static const float ir_1050_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1050_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 9.005889e-01f, -6.452200e-02f, 3.675525e-01f, -4.309962e-01f, 7.086621e-02f, -9.161573e-02f, -4.290351e-02f, 9.057393e-02f, @@ -30597,7 +30605,7 @@ static const float itd_1052_table[HRTF_AZIMUTHS] = { -6.50194f, -5.61262f, -4.72534f, -3.84869f, -2.97504f, -2.10269f, -1.23783f, -0.36766f, }; -static const float ir_1052_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1052_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 6.650009e-01f, 3.507944e-01f, -3.274164e-02f, -1.830690e-01f, -7.720853e-02f, 1.030789e-01f, 3.877069e-02f, -5.674440e-02f, @@ -32052,7 +32060,7 @@ static const float itd_1054_table[HRTF_AZIMUTHS] = { -7.35642f, -6.36606f, -5.37262f, -4.40394f, -3.44967f, -2.51333f, -1.59834f, -0.68300f, }; -static const float ir_1054_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1054_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 8.629450e-01f, 1.677356e-01f, 1.467365e-01f, -3.248726e-01f, -5.105235e-02f, -5.031096e-02f, 1.796471e-01f, -1.298094e-01f, @@ -33507,7 +33515,7 @@ static const float itd_1056_table[HRTF_AZIMUTHS] = { -6.99437f, -5.82430f, -4.73408f, -3.76713f, -2.88870f, -2.05251f, -1.18172f, -0.32736f, }; -static const float ir_1056_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1056_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 8.031418e-01f, 2.411323e-01f, 1.417951e-01f, -2.476192e-01f, -1.076012e-01f, 1.009190e-01f, 7.761394e-02f, -1.250722e-01f, @@ -34962,7 +34970,7 @@ static const float itd_1058_table[HRTF_AZIMUTHS] = { -7.78555f, -6.81447f, -5.85685f, -4.89466f, -3.93902f, -2.98660f, -2.01925f, -1.05758f, }; -static const float ir_1058_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { +ALIGN32 static const float ir_1058_table[HRTF_AZIMUTHS][2][HRTF_TAPS] = { // azimuth = 0 {{ 9.307292e-01f, 5.592706e-02f, 2.567367e-01f, -4.525413e-01f, 1.378666e-01f, -2.503950e-01f, 1.983286e-01f, 5.925522e-03f, diff --git a/libraries/audio/src/avx/AudioHRTF_avx.cpp b/libraries/audio/src/avx/AudioHRTF_avx.cpp deleted file mode 100644 index b103bf015c..0000000000 --- a/libraries/audio/src/avx/AudioHRTF_avx.cpp +++ /dev/null @@ -1,96 +0,0 @@ -// -// AudioHRTF_avx.cpp -// libraries/audio/src/avx -// -// Created by Ken Cooke on 1/17/16. -// Copyright 2016 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 -// - -#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__x86_64__) - -#include -#include - -#include "../AudioHRTF.h" - -#ifndef __AVX__ -#error Must be compiled with /arch:AVX or -mavx. -#endif - -// 1 channel input, 4 channel output -void FIR_1x4_AVX(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames) { - - float* coef0 = coef[0] + HRTF_TAPS - 1; // process backwards - float* coef1 = coef[1] + HRTF_TAPS - 1; - float* coef2 = coef[2] + HRTF_TAPS - 1; - float* coef3 = coef[3] + HRTF_TAPS - 1; - - assert(numFrames % 8 == 0); - - for (int i = 0; i < numFrames; i += 8) { - - __m256 acc0 = _mm256_setzero_ps(); - __m256 acc1 = _mm256_setzero_ps(); - __m256 acc2 = _mm256_setzero_ps(); - __m256 acc3 = _mm256_setzero_ps(); - - float* ps = &src[i - HRTF_TAPS + 1]; // process forwards - - assert(HRTF_TAPS % 8 == 0); - - for (int k = 0; k < HRTF_TAPS; k += 8) { - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-0]), _mm256_loadu_ps(&ps[k+0]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-0]), _mm256_loadu_ps(&ps[k+0]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-0]), _mm256_loadu_ps(&ps[k+0]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-0]), _mm256_loadu_ps(&ps[k+0]))); - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-1]), _mm256_loadu_ps(&ps[k+1]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-1]), _mm256_loadu_ps(&ps[k+1]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-1]), _mm256_loadu_ps(&ps[k+1]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-1]), _mm256_loadu_ps(&ps[k+1]))); - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-2]), _mm256_loadu_ps(&ps[k+2]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-2]), _mm256_loadu_ps(&ps[k+2]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-2]), _mm256_loadu_ps(&ps[k+2]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-2]), _mm256_loadu_ps(&ps[k+2]))); - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-3]), _mm256_loadu_ps(&ps[k+3]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-3]), _mm256_loadu_ps(&ps[k+3]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-3]), _mm256_loadu_ps(&ps[k+3]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-3]), _mm256_loadu_ps(&ps[k+3]))); - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-4]), _mm256_loadu_ps(&ps[k+4]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-4]), _mm256_loadu_ps(&ps[k+4]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-4]), _mm256_loadu_ps(&ps[k+4]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-4]), _mm256_loadu_ps(&ps[k+4]))); - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-5]), _mm256_loadu_ps(&ps[k+5]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-5]), _mm256_loadu_ps(&ps[k+5]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-5]), _mm256_loadu_ps(&ps[k+5]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-5]), _mm256_loadu_ps(&ps[k+5]))); - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-6]), _mm256_loadu_ps(&ps[k+6]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-6]), _mm256_loadu_ps(&ps[k+6]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-6]), _mm256_loadu_ps(&ps[k+6]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-6]), _mm256_loadu_ps(&ps[k+6]))); - - acc0 = _mm256_add_ps(acc0, _mm256_mul_ps(_mm256_broadcast_ss(&coef0[-k-7]), _mm256_loadu_ps(&ps[k+7]))); - acc1 = _mm256_add_ps(acc1, _mm256_mul_ps(_mm256_broadcast_ss(&coef1[-k-7]), _mm256_loadu_ps(&ps[k+7]))); - acc2 = _mm256_add_ps(acc2, _mm256_mul_ps(_mm256_broadcast_ss(&coef2[-k-7]), _mm256_loadu_ps(&ps[k+7]))); - acc3 = _mm256_add_ps(acc3, _mm256_mul_ps(_mm256_broadcast_ss(&coef3[-k-7]), _mm256_loadu_ps(&ps[k+7]))); - } - - _mm256_storeu_ps(&dst0[i], acc0); - _mm256_storeu_ps(&dst1[i], acc1); - _mm256_storeu_ps(&dst2[i], acc2); - _mm256_storeu_ps(&dst3[i], acc3); - } - - _mm256_zeroupper(); -} - -#endif diff --git a/libraries/audio/src/avx2/AudioHRTF_avx2.cpp b/libraries/audio/src/avx2/AudioHRTF_avx2.cpp new file mode 100644 index 0000000000..452ceb7f4c --- /dev/null +++ b/libraries/audio/src/avx2/AudioHRTF_avx2.cpp @@ -0,0 +1,94 @@ +// +// AudioHRTF_avx2.cpp +// libraries/audio/src +// +// Created by Ken Cooke on 1/17/16. +// Copyright 2016 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 +// + +#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__x86_64__) + +#include +#include // AVX2 + +#include "../AudioHRTF.h" + +#ifndef __AVX2__ +#error Must be compiled with /arch:AVX2 or -mavx2 -mfma. +#endif + +#if defined(__GNUC__) && !defined(__clang__) +// for some reason, GCC -O2 results in poorly optimized code +#pragma GCC optimize("Os") +#endif + +// 1 channel input, 4 channel output +void FIR_1x4_AVX2(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames) { + + float* coef0 = coef[0] + HRTF_TAPS - 1; // process backwards + float* coef1 = coef[1] + HRTF_TAPS - 1; + float* coef2 = coef[2] + HRTF_TAPS - 1; + float* coef3 = coef[3] + HRTF_TAPS - 1; + + assert(numFrames % 8 == 0); + + for (int i = 0; i < numFrames; i += 8) { + + __m256 acc0 = _mm256_setzero_ps(); + __m256 acc1 = _mm256_setzero_ps(); + __m256 acc2 = _mm256_setzero_ps(); + __m256 acc3 = _mm256_setzero_ps(); + __m256 acc4 = _mm256_setzero_ps(); + __m256 acc5 = _mm256_setzero_ps(); + __m256 acc6 = _mm256_setzero_ps(); + __m256 acc7 = _mm256_setzero_ps(); + + float* ps = &src[i - HRTF_TAPS + 1]; // process forwards + + assert(HRTF_TAPS % 4 == 0); + + for (int k = 0; k < HRTF_TAPS; k += 4) { + + __m256 x0 = _mm256_loadu_ps(&ps[k+0]); + acc0 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef0[-k-0]), x0, acc0); + acc1 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef1[-k-0]), x0, acc1); + acc2 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef2[-k-0]), x0, acc2); + acc3 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef3[-k-0]), x0, acc3); + + __m256 x1 = _mm256_loadu_ps(&ps[k+1]); + acc4 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef0[-k-1]), x1, acc4); + acc5 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef1[-k-1]), x1, acc5); + acc6 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef2[-k-1]), x1, acc6); + acc7 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef3[-k-1]), x1, acc7); + + __m256 x2 = _mm256_loadu_ps(&ps[k+2]); + acc0 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef0[-k-2]), x2, acc0); + acc1 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef1[-k-2]), x2, acc1); + acc2 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef2[-k-2]), x2, acc2); + acc3 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef3[-k-2]), x2, acc3); + + __m256 x3 = _mm256_loadu_ps(&ps[k+3]); + acc4 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef0[-k-3]), x3, acc4); + acc5 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef1[-k-3]), x3, acc5); + acc6 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef2[-k-3]), x3, acc6); + acc7 = _mm256_fmadd_ps(_mm256_broadcast_ss(&coef3[-k-3]), x3, acc7); + } + + acc0 = _mm256_add_ps(acc0, acc4); + acc1 = _mm256_add_ps(acc1, acc5); + acc2 = _mm256_add_ps(acc2, acc6); + acc3 = _mm256_add_ps(acc3, acc7); + + _mm256_storeu_ps(&dst0[i], acc0); + _mm256_storeu_ps(&dst1[i], acc1); + _mm256_storeu_ps(&dst2[i], acc2); + _mm256_storeu_ps(&dst3[i], acc3); + } + + _mm256_zeroupper(); +} + +#endif