diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 2fbe2f6dfe..2498af8010 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -12,6 +12,8 @@ #include "AssetServer.h" +#include + #include #include #include @@ -21,6 +23,7 @@ #include #include +#include #include #include "NetworkLogging.h" @@ -28,8 +31,44 @@ #include "SendAssetTask.h" #include "UploadAssetTask.h" +static const uint8_t MIN_CORES_FOR_MULTICORE = 4; +static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2; +static const uint8_t CPU_AFFINITY_COUNT_LOW = 1; +static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000; + const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; +bool interfaceRunning() { + bool result = false; + +#ifdef Q_OS_WIN + QSharedMemory sharedMemory { getInterfaceSharedMemoryName() }; + result = sharedMemory.attach(QSharedMemory::ReadOnly); + if (result) { + sharedMemory.detach(); + } +#endif + return result; +} + +void updateConsumedCores() { + static bool wasInterfaceRunning = false; + bool isInterfaceRunning = interfaceRunning(); + // If state is unchanged, return early + if (isInterfaceRunning == wasInterfaceRunning) { + return; + } + + wasInterfaceRunning = isInterfaceRunning; + auto coreCount = std::thread::hardware_concurrency(); + if (isInterfaceRunning) { + coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW; + } + qDebug() << "Setting max consumed cores to " << coreCount; + setMaxCores(coreCount); +} + + AssetServer::AssetServer(ReceivedMessage& message) : ThreadedAssignment(message), _taskPool(this) @@ -45,6 +84,20 @@ AssetServer::AssetServer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation"); + +#ifdef Q_OS_WIN + updateConsumedCores(); + QTimer* timer = new QTimer(this); + auto timerConnection = connect(timer, &QTimer::timeout, [] { + updateConsumedCores(); + }); + connect(qApp, &QCoreApplication::aboutToQuit, [this, timerConnection] { + disconnect(timerConnection); + }); + timer->setInterval(INTERFACE_RUNNING_CHECK_FREQUENCY_MS); + timer->setTimerType(Qt::CoarseTimer); + timer->start(); +#endif } void AssetServer::run() { diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 19ebd4ea87..07639867eb 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -28,6 +28,7 @@ #include #include +#include "AudioHelpers.h" #include "AudioRingBuffer.h" #include "AudioMixerClientData.h" #include "AvatarAudioStream.h" @@ -68,7 +69,8 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket"); packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); - packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); + packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); + packetReceiver.registerListener(PacketType::PerAvatarGainSet, this, "handlePerAvatarGainSetDataPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -186,7 +188,8 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { nodeList->eachNode([&killedNode](const SharedNodePointer& node) { auto clientData = dynamic_cast(node->getLinkedData()); if (clientData) { - clientData->removeHRTFsForNode(killedNode->getUUID()); + QUuid killedUUID = killedNode->getUUID(); + clientData->removeHRTFsForNode(killedUUID); } }); } @@ -240,6 +243,20 @@ void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer p sendingNode->parseIgnoreRequestMessage(packet); } +void AudioMixer::handlePerAvatarGainSetDataPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + auto clientData = dynamic_cast(sendingNode->getLinkedData()); + if (clientData) { + QUuid listeningNodeUUID = sendingNode->getUUID(); + // parse the UUID from the packet + QUuid audioSourceUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + uint8_t packedGain; + packet->readPrimitive(&packedGain); + float gain = unpackFloatGainFromByte(packedGain); + clientData->hrtfForStream(audioSourceUUID, QUuid()).setGainAdjustment(gain); + qDebug() << "Setting gain adjustment for hrtf[" << listeningNodeUUID << "][" << audioSourceUUID << "] to " << gain; + } +} + void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { sendingNode->parseIgnoreRadiusRequestMessage(packet); } diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index d9759653fb..d88bc3b917 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -66,6 +66,7 @@ private slots: void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNodeMuteRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handlePerAvatarGainSetDataPacket(QSharedPointer packet, SharedNodePointer sendingNode); void start(); void removeHRTFsForFinishedInjector(const QUuid& streamID); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 11cbd73970..cd866cecb2 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -262,8 +262,12 @@ void AvatarMixer::broadcastAvatarData() { // setup a PacketList for the avatarPackets auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData); - if (avatar.getSessionDisplayName().isEmpty() && // We haven't set it yet... - nodeData->getReceivedIdentity()) { // ... but we have processed identity (with possible displayName). + if (nodeData->getAvatarSessionDisplayNameMustChange()) { + const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); + if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) { + _sessionDisplayNames.remove(existingBaseDisplayName); + } + QString baseName = avatar.getDisplayName().trimmed(); const QRegularExpression curses{ "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?). baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk. @@ -276,11 +280,14 @@ void AvatarMixer::broadcastAvatarData() { QPair& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want. int& highWater = soFar.first; nodeData->setBaseDisplayName(baseName); - avatar.setSessionDisplayName((highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName); + QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName; + avatar.setSessionDisplayName(sessionDisplayName); highWater++; soFar.second++; // refcount nodeData->flagIdentityChange(); - sendIdentityPacket(nodeData, node); // Tell new node about its sessionUUID. Others will find out below. + nodeData->setAvatarSessionDisplayNameMustChange(false); + sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. Others will find out below. + qDebug() << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID(); } // this is an AGENT we have received head data from @@ -584,7 +591,7 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes if (avatar.processAvatarIdentity(identity)) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->flagIdentityChange(); - nodeData->setReceivedIdentity(); + nodeData->setAvatarSessionDisplayNameMustChange(true); } } } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index f18cfdde1b..38db2e74d2 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -53,8 +53,8 @@ public: HRCTime getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } void flagIdentityChange() { _identityChangeTimestamp = p_high_resolution_clock::now(); } - bool getReceivedIdentity() const { return _gotIdentity; } - void setReceivedIdentity() { _gotIdentity = true; } + bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; } + void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; } void setFullRateDistance(float fullRateDistance) { _fullRateDistance = fullRateDistance; } float getFullRateDistance() const { return _fullRateDistance; } @@ -112,7 +112,7 @@ private: std::unordered_set _hasReceivedFirstPacketsFrom; HRCTime _identityChangeTimestamp; - bool _gotIdentity { false }; + bool _avatarSessionDisplayNameMustChange{ false }; float _fullRateDistance = FLT_MAX; float _maxAvatarDistance = FLT_MAX; diff --git a/cmake/externals/LibOVRPlatform/CMakeLists.txt b/cmake/externals/LibOVRPlatform/CMakeLists.txt new file mode 100644 index 0000000000..3622972a13 --- /dev/null +++ b/cmake/externals/LibOVRPlatform/CMakeLists.txt @@ -0,0 +1,32 @@ +include(ExternalProject) +include(SelectLibraryConfigurations) + +set(EXTERNAL_NAME LibOVRPlatform) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +if (WIN32) + + ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://hifi-public.s3.amazonaws.com/dependencies/OVRPlatformSDK_v1.10.0.zip + URL_MD5 e6c8264af16d904e6506acd5172fa0a9 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 + ) + + ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) + + if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/Windows/LibOVRPlatform64_1.lib CACHE TYPE INTERNAL) + else() + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/Windows/LibOVRPlatform32_1.lib CACHE TYPE INTERNAL) + endif() + + set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/Include CACHE TYPE INTERNAL) +endif () + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") diff --git a/cmake/modules/FindLibOVRPlatform.cmake b/cmake/modules/FindLibOVRPlatform.cmake new file mode 100644 index 0000000000..e9caa2cb98 --- /dev/null +++ b/cmake/modules/FindLibOVRPlatform.cmake @@ -0,0 +1,44 @@ +# +# FindLibOVRPlatform.cmake +# +# Try to find the LibOVRPlatform library to use the Oculus Platform SDK +# +# You must provide a LIBOVRPLATFORM_ROOT_DIR which contains Windows and Include directories +# +# Once done this will define +# +# LIBOVRPLATFORM_FOUND - system found Oculus Platform SDK +# LIBOVRPLATFORM_INCLUDE_DIRS - the Oculus Platform include directory +# LIBOVRPLATFORM_LIBRARIES - Link this to use Oculus Platform +# +# Created on December 16, 2016 by Stephen Birarda +# 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 (WIN32) + # setup hints for LIBOVRPLATFORM search + include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") + hifi_library_search_hints("LibOVRPlatform") + + find_path(LIBOVRPLATFORM_INCLUDE_DIRS OVR_Platform.h PATH_SUFFIXES Include HINTS ${LIBOVRPLATFORM_SEARCH_DIRS}) + + if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + set(_LIB_NAME LibOVRPlatform64_1.lib) + else() + set(_LIB_NAME LibOVRPlatform32_1.lib) + endif() + + find_library(LIBOVRPLATFORM_LIBRARY_RELEASE NAMES ${_LIB_NAME} PATH_SUFFIXES Windows HINTS ${LIBOVRPLATFORM_SEARCH_DIRS}) + + include(SelectLibraryConfigurations) + select_library_configurations(LIBOVRPLATFORM) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(LIBOVRPLATFORM DEFAULT_MSG LIBOVRPLATFORM_INCLUDE_DIRS LIBOVRPLATFORM_LIBRARIES) + + mark_as_advanced(LIBOVRPLATFORM_INCLUDE_DIRS LIBOVRPLATFORM_LIBRARIES LIBOVRPLATFORM_SEARCH_DIRS) +endif () diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index f701f35433..495c1a1571 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -40,7 +40,7 @@ { "name": "local_port", "label": "Local UDP Port", - "help": "This is the local port your domain-server binds to for UDP connections.
Depending on your router, this may need to be changed to run multiple full automatic networking domain-servers in the same network.", + "help": "This is the local port your domain-server binds to for UDP connections.
Depending on your router, this may need to be changed to unique values for each domain-server in order to run multiple full automatic networking domain-servers in the same network. You can use the value 0 to have the domain-server select a random port, which will help in preventing port collisions.", "default": "40102", "type": "int", "advanced": true diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index c8895839ff..c6bcc5af34 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -780,12 +780,12 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer message, SharedNodePointer sendingNode) { - // Before we do any processing on this packet, make sure it comes from a node that is allowed to kick (is an admin) - if (sendingNode->getCanKick()) { - // From the packet, pull the UUID we're identifying - QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - - if (!nodeUUID.isNull()) { + // From the packet, pull the UUID we're identifying + QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + if (!nodeUUID.isNull()) { + // Before we do any processing on this packet, make sure it comes from a node that is allowed to kick (is an admin) + // OR from a node whose UUID matches the one in the packet + if (sendingNode->getCanKick() || nodeUUID == sendingNode->getUUID()) { // First, make sure we actually have a node with this UUID auto limitedNodeList = DependencyManager::get(); auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID); @@ -813,12 +813,12 @@ void DomainServerSettingsManager::processUsernameFromIDRequestPacket(QSharedPoin qWarning() << "Node username request received for unknown node. Refusing to process."; } } else { - qWarning() << "Node username request received for invalid node ID. Refusing to process."; + qWarning() << "Refusing to process a username request packet from node" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()) + << ". Either node doesn't have kick permissions or is requesting a username not from their UUID."; } } else { - qWarning() << "Refusing to process a username request packet from node" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()) - << "that does not have kick permissions."; + qWarning() << "Node username request received for invalid node ID. Refusing to process."; } } diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 09aefffdfe..c139a196d0 100644 Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index a0dde8bacc..2725ea1617 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -10,35 +10,35 @@ // import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import "../styles-uit" -Row { +Item { id: thisNameCard - // Spacing - spacing: 10 // Anchors - anchors.top: parent.top anchors { - topMargin: (parent.height - contentHeight)/2 - bottomMargin: (parent.height - contentHeight)/2 + verticalCenter: parent.verticalCenter leftMargin: 10 rightMargin: 10 } // Properties - property int contentHeight: 50 + property string uuid: "" property string displayName: "" property string userName: "" - property int displayTextHeight: 18 + property real displayNameTextPixelSize: 18 property int usernameTextHeight: 12 property real audioLevel: 0.0 + property bool isMyCard: false + property bool selected: false /* User image commented out for now - will probably be re-introduced later. Column { id: avatarImage // Size - height: contentHeight + height: parent.height width: height Image { id: userImage @@ -49,22 +49,118 @@ Row { } } */ - Column { + Item { id: textContainer // Size - width: parent.width - /*avatarImage.width - */parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing - height: contentHeight + width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin + height: childrenRect.height + anchors.verticalCenter: parent.verticalCenter - // DisplayName Text + // DisplayName field for my card + Rectangle { + id: myDisplayName + visible: isMyCard + // Size + width: parent.width + 70 + height: 35 + // Anchors + anchors.top: parent.top + anchors.left: parent.left + anchors.leftMargin: -10 + // Style + color: hifi.colors.textFieldLightBackground + border.color: hifi.colors.blueHighlight + border.width: 0 + TextInput { + id: myDisplayNameText + // Properties + text: thisNameCard.displayName + maximumLength: 256 + clip: true + // Size + width: parent.width + height: parent.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin + // Style + color: hifi.colors.darkGray + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: displayNameTextPixelSize + selectionColor: hifi.colors.blueHighlight + selectedTextColor: "black" + // Text Positioning + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + // Signals + onEditingFinished: { + pal.sendToScript({method: 'displayNameUpdate', params: text}) + cursorPosition = 0 + focus = false + myDisplayName.border.width = 0 + color = hifi.colors.darkGray + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onClicked: { + myDisplayName.border.width = 1 + myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); + myDisplayNameText.focus = true + myDisplayNameText.color = "black" + } + onDoubleClicked: { + myDisplayNameText.selectAll(); + myDisplayNameText.focus = true; + } + onEntered: myDisplayName.color = hifi.colors.lightGrayText + onExited: myDisplayName.color = hifi.colors.textFieldLightBackground + } + // Edit pencil glyph + HiFiGlyphs { + id: editGlyph + text: hifi.glyphs.editPencil + // Text Size + size: displayNameTextPixelSize*1.5 + // Anchors + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter + // Style + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: hifi.colors.baseGray + } + } + // Spacer for DisplayName for my card + Rectangle { + id: myDisplayNameSpacer + width: myDisplayName.width + // Anchors + anchors.top: myDisplayName.bottom + height: 5 + visible: isMyCard + opacity: 0 + } + // DisplayName Text for others' cards FiraSansSemiBold { id: displayNameText // Properties text: thisNameCard.displayName elide: Text.ElideRight + visible: !isMyCard // Size width: parent.width + // Anchors + anchors.top: parent.top // Text Size - size: thisNameCard.displayTextHeight + size: displayNameTextPixelSize // Text Positioning verticalAlignment: Text.AlignVCenter // Style @@ -80,6 +176,8 @@ Row { visible: thisNameCard.displayName // Size width: parent.width + // Anchors + anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameText.bottom // Text Size size: thisNameCard.usernameTextHeight // Text Positioning @@ -90,25 +188,56 @@ Row { // Spacer Item { + id: spacer height: 4 width: parent.width + // Anchors + anchors.top: userNameText.bottom } // VU Meter - Rectangle { // CHANGEME to the appropriate type! + Rectangle { id: nameCardVUMeter // Size - width: parent.width + width: isMyCard ? myDisplayName.width - 20 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width height: 8 + // Anchors + anchors.top: spacer.bottom // Style radius: 4 + color: "#c5c5c5" + // Rectangle for the zero-gain point on the VU meter + Rectangle { + id: vuMeterZeroGain + visible: gainSlider.visible + // Size + width: 4 + height: 18 + // Style + color: hifi.colors.darkGray + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4 + } + // Rectangle for the VU meter line + Rectangle { + id: vuMeterLine + width: gainSlider.width + visible: gainSlider.visible + // Style + color: vuMeterBase.color + radius: nameCardVUMeter.radius + height: nameCardVUMeter.height / 2 + anchors.verticalCenter: nameCardVUMeter.verticalCenter + } // Rectangle for the VU meter base Rectangle { id: vuMeterBase // Anchors anchors.fill: parent // Style - color: "#c5c5c5" + color: parent.color radius: parent.radius } // Rectangle for the VU meter audio level @@ -117,7 +246,7 @@ Row { // Size width: (thisNameCard.audioLevel) * parent.width // Style - color: "#c5c5c5" + color: parent.color radius: parent.radius // Anchors anchors.bottom: parent.bottom @@ -138,5 +267,66 @@ Row { } } } + + // Per-Avatar Gain Slider + Slider { + id: gainSlider + // Size + width: parent.width + height: 14 + // Anchors + anchors.verticalCenter: nameCardVUMeter.verticalCenter + // Properties + visible: !isMyCard && selected + value: pal.gainSliderValueDB[uuid] ? pal.gainSliderValueDB[uuid] : 0.0 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + updateValueWhileDragging: true + onValueChanged: updateGainFromQML(uuid, value) + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + gainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // Pass through to Slider + mouse.accepted = false + } + } + style: SliderStyle { + groove: Rectangle { + color: "#c5c5c5" + implicitWidth: gainSlider.width + implicitHeight: 4 + radius: 2 + opacity: 0 + } + handle: Rectangle { + anchors.centerIn: parent + color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F" + implicitWidth: 10 + implicitHeight: 16 + } + } + } + } + + function updateGainFromQML(avatarUuid, sliderValue) { + if (pal.gainSliderValueDB[avatarUuid] !== sliderValue) { + pal.gainSliderValueDB[avatarUuid] = sliderValue; + var data = { + sessionId: avatarUuid, + gain: sliderValue + }; + pal.sendToScript({method: 'updateGain', params: data}); + } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 66dce622ff..549598dd2e 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -24,7 +24,7 @@ Rectangle { // Style color: "#E3E3E3" // Properties - property int myCardHeight: 70 + property int myCardHeight: 90 property int rowHeight: 70 property int actionButtonWidth: 75 property int nameCardWidth: palContainer.width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth @@ -32,6 +32,20 @@ Rectangle { property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. property bool iAmAdmin: false + // Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server. + // NOTE: if another script modifies the per-avatar gain, this value won't be accurate! + property var gainSliderValueDB: ({}); + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage + z: 999 // Force the popup on top of everything else + } + function letterbox(message) { + letterboxMessage.text = message + letterboxMessage.visible = true + letterboxMessage.popupRadius = 0 + } // This is the container for the PAL Rectangle { @@ -51,7 +65,7 @@ Rectangle { id: myInfo // Size width: palContainer.width - height: myCardHeight + 20 + height: myCardHeight // Style color: pal.color // Anchors @@ -65,6 +79,7 @@ Rectangle { displayName: myData.displayName userName: myData.userName audioLevel: myData.audioLevel + isMyCard: true // Size width: nameCardWidth height: parent.height @@ -172,8 +187,6 @@ Rectangle { TableViewColumn { visible: iAmAdmin role: "kick" - // The hacky spaces used to center text over the button, since I don't know how to apply a margin - // to column header text. title: "BAN" width: actionButtonWidth movable: false @@ -206,6 +219,8 @@ Rectangle { userName: model && model.userName audioLevel: model && model.audioLevel visible: !isCheckBox && !isButton + uuid: model && model.sessionId + selected: styleData.selected // Size width: nameCardWidth height: parent.height @@ -331,11 +346,6 @@ Rectangle { visible: iAmAdmin color: hifi.colors.lightGrayText } - function letterbox(message) { - letterboxMessage.text = message; - letterboxMessage.visible = true - - } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { color: hifi.colors.tableBackgroundLight @@ -396,9 +406,6 @@ Rectangle { onExited: adminHelpText.color = hifi.colors.redHighlight } } - LetterboxMessage { - id: letterboxMessage - } } function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml @@ -492,8 +499,13 @@ Rectangle { } } break; - case 'clearIgnored': + case 'clearLocalQMLData': ignored = {}; + gainSliderValueDB = {}; + break; + case 'avatarDisconnected': + var sessionID = message.params[0]; + delete ignored[sessionID]; break; default: console.log('Unrecognized message:', JSON.stringify(message)); diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 7eca1aa725..18bdd89799 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -312,10 +312,11 @@ Item { readonly property string error: "=" readonly property string settings: "@" readonly property string trash: "{" - readonly property string objectGroup: "" + readonly property string objectGroup: "\ue000" readonly property string cm: "}" readonly property string msvg79: "~" readonly property string deg: "\\" readonly property string px: "|" + readonly property string editPencil: "\ue00d" } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index dd9c097319..4f35b10a1b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -252,7 +252,7 @@ public: static const unsigned long MAX_HEARTBEAT_AGE_USECS = 30 * USECS_PER_SECOND; static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples - + // Set the heartbeat on launch DeadlockWatchdogThread() { setObjectName("Deadlock Watchdog"); @@ -616,7 +616,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _window->setWindowTitle("Interface"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us - + auto nodeList = DependencyManager::get(); // Set up a watchdog thread to intentionally crash the application on deadlocks @@ -637,6 +637,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; #endif + // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store + static const QString OCULUS_STORE_ARG = "--oculus-store"; + setProperty(hifi::properties::OCULUS_STORE, arguments().indexOf(OCULUS_STORE_ARG) != -1); static const QString NO_UPDATER_ARG = "--no-updater"; static const bool noUpdater = arguments().indexOf(NO_UPDATER_ARG) != -1; @@ -697,7 +700,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo audioIO->setPositionGetter([]{ auto avatarManager = DependencyManager::get(); auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - + return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; }); audioIO->setOrientationGetter([]{ @@ -880,7 +883,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo #ifdef Q_OS_MAC auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget #else - // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the + // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the // window menu, which is a pain, so only hide it for the GL surface auto cursorTarget = _glWidget; #endif @@ -1125,7 +1128,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo loadSettings(); // Now that we've loaded the menu and thus switched to the previous display plugin - // we can unlock the desktop repositioning code, since all the positions will be + // we can unlock the desktop repositioning code, since all the positions will be // relative to the desktop size for this plugin auto offscreenUi = DependencyManager::get(); offscreenUi->getDesktop()->setProperty("repositionLocked", false); @@ -1235,8 +1238,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Add periodic checks to send user activity data static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; - static int SEND_STATS_INTERVAL_MS = 10000; static int NEARBY_AVATAR_RADIUS_METERS = 10; + + // setup the stats interval depending on if the 1s faster hearbeat was requested + static const QString FAST_STATS_ARG = "--fast-heartbeat"; + static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; static glm::vec3 lastAvatarPosition = myAvatar->getPosition(); static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); @@ -1598,7 +1604,7 @@ void Application::checkChangeCursor() { #ifdef Q_OS_MAC auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget #else - // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the + // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the // window menu, which is a pain, so only hide it for the GL surface auto cursorTarget = _glWidget; #endif @@ -1785,7 +1791,7 @@ Application::~Application() { #endif // The window takes ownership of the menu, so this has the side effect of destroying it. _window->setMenuBar(nullptr); - + _window->deleteLater(); // Can't log to file passed this point, FileLogger about to be deleted @@ -1811,10 +1817,10 @@ void Application::initializeGL() { _glWidget->makeCurrent(); gpu::Context::init(); - qApp->setProperty(hifi::properties::gl::MAKE_PROGRAM_CALLBACK, + qApp->setProperty(hifi::properties::gl::MAKE_PROGRAM_CALLBACK, QVariant::fromValue((void*)(&gpu::gl::GLBackend::makeProgram))); _gpuContext = std::make_shared(); - // The gpu context can make child contexts for transfers, so + // The gpu context can make child contexts for transfers, so // we need to restore primary rendering context _glWidget->makeCurrent(); @@ -2034,7 +2040,7 @@ void Application::paintGL() { // FIXME not needed anymore? _offscreenContext->makeCurrent(); - // If a display plugin loses it's underlying support, it + // If a display plugin loses it's underlying support, it // needs to be able to signal us to not use it if (!displayPlugin->beginFrameRender(_frameCount)) { _inPaint = false; @@ -2846,7 +2852,7 @@ void Application::keyPressEvent(QKeyEvent* event) { if (isMirrorChecked) { // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old + // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old // behavior of returning to ThirdPerson if (_returnFromFullScreenMirrorTo.isEmpty()) { _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; @@ -3020,7 +3026,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) { maybeToggleMenuVisible(event); auto& compositor = getApplicationCompositor(); - // if this is a real mouse event, and we're in HMD mode, then we should use it to move the + // if this is a real mouse event, and we're in HMD mode, then we should use it to move the // compositor reticle // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { @@ -4109,7 +4115,7 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) { } _lastAcceptedKeyPress = usecTimestampNow(); - setKeyboardFocusHighlight(entity->getPosition(), entity->getRotation(), + setKeyboardFocusHighlight(entity->getPosition(), entity->getRotation(), entity->getDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); } } @@ -4700,7 +4706,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node _octreeQuery.setMaxQueryPacketsPerSecond(0); } - // if asked to forceResend, then set the query's position/orientation to be degenerate in a manner + // if asked to forceResend, then set the query's position/orientation to be degenerate in a manner // that will cause our next query to be guarenteed to be different and the server will resend to us if (forceResend) { _octreeQuery.setCameraPosition(glm::vec3(-0.1, -0.1, -0.1)); @@ -5294,15 +5300,17 @@ bool Application::nearbyEntitiesAreReadyForPhysics() { if (_nearbyEntitiesStabilityCount >= MINIMUM_NEARBY_ENTITIES_STABILITY_COUNT) { // We've seen the same number of nearby entities for several stats packets in a row. assume we've got all // the local entities. + bool result = true; foreach (EntityItemPointer entity, entities) { if (entity->shouldBePhysical() && !entity->isReadyToComputeShape()) { static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex("Physics disabled until entity loads: .*"); qCDebug(interfaceapp) << "Physics disabled until entity loads: " << entity->getID() << entity->getName(); - return false; + // don't break here because we want all the relevant entities to start their downloads + result = false; } } - return true; + return result; } return false; } @@ -5833,7 +5841,7 @@ void Application::addAssetToWorldWithNewMapping(QString filePath, QString mappin mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy)); addAssetToWorldWithNewMapping(filePath, mapping, copy); } else { - QString errorInfo = "Too many copies of asset name: " + QString errorInfo = "Too many copies of asset name: " + mapping.left(mapping.length() - QString::number(copy).length() - 1); qWarning(interfaceapp) << "Error downloading model: " + errorInfo; addAssetToWorldError(filenameFromPath(filePath), errorInfo); @@ -5900,7 +5908,7 @@ void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) { // Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later // on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted. - + if (entityID == QUuid()) { QString errorInfo = "Could not add model " + mapping + " to world."; qWarning(interfaceapp) << "Could not add model to world: " + errorInfo; @@ -6364,7 +6372,7 @@ glm::uvec2 Application::getCanvasSize() const { } QRect Application::getRenderingGeometry() const { - auto geometry = _glWidget->geometry(); + auto geometry = _glWidget->geometry(); auto topLeft = geometry.topLeft(); auto topLeftScreen = _glWidget->mapToGlobal(topLeft); geometry.moveTopLeft(topLeftScreen); @@ -6727,8 +6735,8 @@ bool Application::makeRenderingContextCurrent() { return _offscreenContext->makeCurrent(); } -bool Application::isForeground() const { - return _isForeground && !_window->isMinimized(); +bool Application::isForeground() const { + return _isForeground && !_window->isMinimized(); } void Application::sendMousePressOnEntity(QUuid id, PointerEvent event) { diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1f5726acba..b0dc9922ff 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -260,6 +260,10 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar } if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == YourAvatarEnteredTheirBubble) { DependencyManager::get()->radiusIgnoreNodeBySessionID(avatar->getSessionUUID(), true); + } else if (removalReason == KillAvatarReason::AvatarDisconnected) { + // remove from node sets, if present + DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); + DependencyManager::get()->avatarDisconnected(avatar->getSessionUUID()); } _avatarFades.push_back(removedAvatar); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index c83f7a989c..d33dba535e 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -56,7 +56,7 @@ int main(int argc, const char* argv[]) { QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); QCoreApplication::setApplicationVersion(BuildInfo::VERSION); - QString applicationName = "High Fidelity Interface - " + qgetenv("USERNAME"); + const QString& applicationName = getInterfaceSharedMemoryName(); bool instanceMightBeRunning = true; diff --git a/libraries/audio/src/AudioHRTF.h b/libraries/audio/src/AudioHRTF.h index c9d053bec4..6a17a2d3cc 100644 --- a/libraries/audio/src/AudioHRTF.h +++ b/libraries/audio/src/AudioHRTF.h @@ -45,7 +45,7 @@ public: void renderSilent(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames); // - // HRTF local gain adjustment + // HRTF local gain adjustment in amplitude (1.0 == unity) // void setGainAdjustment(float gain) { _gainAdjust = HRTF_GAIN * gain; }; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index f8805d3fc4..1fb68fce14 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1047,7 +1047,7 @@ bool AvatarData::processAvatarIdentity(const Identity& identity) { } if (identity.displayName != _displayName) { - setDisplayName(identity.displayName); + _displayName = identity.displayName; hasIdentityChanged = true; } maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName); @@ -1094,6 +1094,9 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { void AvatarData::setDisplayName(const QString& displayName) { _displayName = displayName; + _sessionDisplayName = ""; + + sendIdentityPacket(); qCDebug(avatars) << "Changing display name for avatar to" << displayName; } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index b901db38e7..55c766c267 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -646,6 +646,12 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { // the model is still being downloaded. return false; } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { + if (!_model) { + EntityTreePointer tree = getTree(); + if (tree) { + QMetaObject::invokeMethod(tree.get(), "callLoader", Qt::QueuedConnection, Q_ARG(EntityItemID, getID())); + } + } return (_model && _model->isLoaded()); } return true; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 5272969e6b..42922ce226 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -972,7 +972,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS static const QVariant EMISSIVE = QByteArray("Emissive"); static const QVariant AMBIENT_FACTOR = QByteArray("AmbientFactor"); static const QVariant SHININESS = QByteArray("Shininess"); - static const QVariant OPACITY = QByteArray("Shininess"); + static const QVariant OPACITY = QByteArray("Opacity"); static const QVariant MAYA_USE_NORMAL_MAP = QByteArray("Maya|use_normal_map"); static const QVariant MAYA_BASE_COLOR = QByteArray("Maya|base_color"); static const QVariant MAYA_USE_COLOR_MAP = QByteArray("Maya|use_color_map"); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index d890431a45..0fcd207b94 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -26,6 +26,7 @@ #include "AccountManager.h" #include "AddressManager.h" #include "Assignment.h" +#include "AudioHelpers.h" #include "HifiSockAddr.h" #include "FingerprintUtils.h" @@ -846,6 +847,16 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { } } +void NodeList::removeFromIgnoreMuteSets(const QUuid& nodeID) { + // don't remove yourself, or nobody + if (!nodeID.isNull() && _sessionUUID != nodeID) { + QWriteLocker ignoredSetLocker{ &_ignoredSetLock }; + QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; + _ignoredNodeIDs.unsafe_erase(nodeID); + _personalMutedNodeIDs.unsafe_erase(nodeID); + } +} + bool NodeList::isIgnoringNode(const QUuid& nodeID) const { QReadLocker ignoredSetLocker{ &_ignoredSetLock }; return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend(); @@ -951,6 +962,30 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { } } +void NodeList::setAvatarGain(const QUuid& nodeID, float gain) { + // cannot set gain of yourself or nobody + if (!nodeID.isNull() && _sessionUUID != nodeID) { + auto audioMixer = soloNodeOfType(NodeType::AudioMixer); + if (audioMixer) { + // setup the packet + auto setAvatarGainPacket = NLPacket::create(PacketType::PerAvatarGainSet, NUM_BYTES_RFC4122_UUID + sizeof(float), true); + + // write the node ID to the packet + setAvatarGainPacket->write(nodeID.toRfc4122()); + // We need to convert the gain in dB (from the script) to an amplitude before packing it. + setAvatarGainPacket->writePrimitive(packFloatGainToByte(fastExp2f(gain / 6.0206f))); + + qCDebug(networking) << "Sending Set Avatar Gain packet UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; + + sendPacket(std::move(setAvatarGainPacket), *audioMixer); + } else { + qWarning() << "Couldn't find audio mixer to send set gain request"; + } + } else { + qWarning() << "NodeList::setAvatarGain called with an invalid ID or an ID which matches the current session ID:" << nodeID; + } +} + void NodeList::kickNodeBySessionID(const QUuid& nodeID) { // send a request to domain-server to kick the node with the given session ID // the domain-server will handle the persistence of the kick (via username or IP) diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 8e285629dc..c4564c0889 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -82,6 +82,7 @@ public: bool isIgnoringNode(const QUuid& nodeID) const; void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); bool isPersonalMutingNode(const QUuid& nodeID) const; + void setAvatarGain(const QUuid& nodeID, float gain); void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); @@ -89,6 +90,8 @@ public: bool getRequestsDomainListData() { return _requestsDomainListData; } void setRequestsDomainListData(bool isRequesting); + void removeFromIgnoreMuteSets(const QUuid& nodeID); + public slots: void reset(); void sendDomainServerCheckIn(); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e4c4937622..5cef6013d9 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -106,7 +106,8 @@ public: ViewFrustum, RequestsDomainListData, ExitingSpaceBubble, - LAST_PACKET_TYPE = ExitingSpaceBubble + PerAvatarGainSet, + LAST_PACKET_TYPE = PerAvatarGainSet }; }; @@ -210,7 +211,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { HandControllerJoints, HasKillAvatarReason, SessionDisplayName, - Unignore + Unignore, + ImmediateSessionDisplayNameUpdates }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 5c4c0890a7..e3b2527e67 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -359,8 +359,11 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transform, const QVector& clusterMatrices) { +void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transform, + const QVector& clusterMatrices, + const QVector& cauterizedClusterMatrices) { _transform = transform; + _cauterizedTransform = transform; if (clusterMatrices.size() > 0) { _worldBound = AABox(); @@ -373,6 +376,11 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transf _worldBound.transform(transform); if (clusterMatrices.size() == 1) { _transform = _transform.worldTransform(Transform(clusterMatrices[0])); + if (cauterizedClusterMatrices.size() != 0) { + _cauterizedTransform = _cauterizedTransform.worldTransform(Transform(cauterizedClusterMatrices[0])); + } else { + _cauterizedTransform = _transform; + } } } } @@ -527,9 +535,14 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline: } else { batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); } + batch.setModelTransform(_transform); + } else { + if (canCauterize && _model->getCauterizeBones()) { + batch.setModelTransform(_cauterizedTransform); + } else { + batch.setModelTransform(_transform); + } } - - batch.setModelTransform(_transform); } void ModelMeshPartPayload::startFade() { diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index b048dc903f..b7a8cf63f0 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -85,7 +85,9 @@ public: typedef Payload::DataPointer Pointer; void notifyLocationChanged() override; - void updateTransformForSkinnedMesh(const Transform& transform, const QVector& clusterMatrices); + void updateTransformForSkinnedMesh(const Transform& transform, + const QVector& clusterMatrices, + const QVector& cauterizedClusterMatrices); // Entity fade in void startFade(); @@ -106,6 +108,7 @@ public: Model* _model; + Transform _cauterizedTransform; int _meshIndex; int _shapeID; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 7c1a6e14d0..3a3095458c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -238,7 +238,7 @@ void Model::updateRenderItems() { // update the model transform and bounding box for this render item. const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); - data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices); + data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices, state.cauterizedClusterMatrices); } } }); diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index d0ad699846..3a3225ec75 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -42,6 +42,11 @@ bool UsersScriptingInterface::getPersonalMuteStatus(const QUuid& nodeID) { return DependencyManager::get()->isPersonalMutingNode(nodeID); } +void UsersScriptingInterface::setAvatarGain(const QUuid& nodeID, float gain) { + // ask the NodeList to set the gain of the specified avatar + DependencyManager::get()->setAvatarGain(nodeID, gain); +} + void UsersScriptingInterface::kick(const QUuid& nodeID) { // ask the NodeList to kick the user with the given session ID DependencyManager::get()->kickNodeBySessionID(nodeID); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 341285a742..758868ac63 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -61,6 +61,15 @@ public slots: */ bool getPersonalMuteStatus(const QUuid& nodeID); + /**jsdoc + * Sets an avatar's gain for you and you only. + * Units are Decibels (dB) + * @function Users.setAvatarGain + * @param {nodeID} nodeID The node or session ID of the user whose gain you want to modify. + * @param {float} gain The gain of the avatar you'd like to set. Units are dB. + */ + void setAvatarGain(const QUuid& nodeID, float gain); + /**jsdoc * Kick another user. * @function Users.kick @@ -130,6 +139,13 @@ signals: */ void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); + /**jsdoc + * Notifies scripts that a user has disconnected from the domain + * @function Users.avatar.avatarDisconnected + * @param {nodeID} NodeID The session ID of the avatar that has disconnected + */ + void avatarDisconnected(const QUuid& nodeID); + private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 89116aa924..a7e251f0e0 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include @@ -1022,4 +1024,54 @@ bool getProcessorInfo(ProcessorInfo& info) { #endif return false; -} \ No newline at end of file +} + + +const QString& getInterfaceSharedMemoryName() { + static const QString applicationName = "High Fidelity Interface - " + qgetenv("USERNAME"); + return applicationName; +} + +const std::vector& getAvailableCores() { + static std::vector availableCores; +#ifdef Q_OS_WIN + static std::once_flag once; + std::call_once(once, [&] { + DWORD_PTR defaultProcessAffinity = 0, defaultSystemAffinity = 0; + HANDLE process = GetCurrentProcess(); + GetProcessAffinityMask(process, &defaultProcessAffinity, &defaultSystemAffinity); + for (uint64_t i = 0; i < sizeof(DWORD_PTR) * BITS_IN_BYTE; ++i) { + DWORD_PTR coreMask = 1; + coreMask <<= i; + if (0 != (defaultSystemAffinity & coreMask)) { + availableCores.push_back(i); + } + } + }); +#endif + return availableCores; +} + +void setMaxCores(uint8_t maxCores) { +#ifdef Q_OS_WIN + HANDLE process = GetCurrentProcess(); + auto availableCores = getAvailableCores(); + if (availableCores.size() <= maxCores) { + DWORD_PTR currentProcessAffinity = 0, currentSystemAffinity = 0; + GetProcessAffinityMask(process, ¤tProcessAffinity, ¤tSystemAffinity); + SetProcessAffinityMask(GetCurrentProcess(), currentSystemAffinity); + return; + } + + DWORD_PTR newProcessAffinity = 0; + while (maxCores) { + int index = randIntInRange(0, (int)availableCores.size() - 1); + DWORD_PTR coreMask = 1; + coreMask <<= availableCores[index]; + newProcessAffinity |= coreMask; + availableCores.erase(availableCores.begin() + index); + maxCores--; + } + SetProcessAffinityMask(process, newProcessAffinity); +#endif +} diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 77204c9608..0f30842f47 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -231,4 +231,8 @@ struct ProcessorInfo { bool getProcessorInfo(ProcessorInfo& info); +const QString& getInterfaceSharedMemoryName(); + +void setMaxCores(uint8_t maxCores); + #endif // hifi_SharedUtil_h diff --git a/libraries/shared/src/shared/GlobalAppProperties.cpp b/libraries/shared/src/shared/GlobalAppProperties.cpp index a8a38457f2..f2d8990708 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.cpp +++ b/libraries/shared/src/shared/GlobalAppProperties.cpp @@ -13,6 +13,7 @@ namespace hifi { namespace properties { const char* CRASHED = "com.highfidelity.crashed"; const char* STEAM = "com.highfidelity.launchedFromSteam"; const char* LOGGER = "com.highfidelity.logger"; + const char* OCULUS_STORE = "com.highfidelity.oculusStore"; const char* TEST = "com.highfidelity.test"; const char* TRACING = "com.highfidelity.tracing"; diff --git a/libraries/shared/src/shared/GlobalAppProperties.h b/libraries/shared/src/shared/GlobalAppProperties.h index 92d8388f09..609f2afd94 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.h +++ b/libraries/shared/src/shared/GlobalAppProperties.h @@ -15,6 +15,7 @@ namespace hifi { namespace properties { extern const char* CRASHED; extern const char* STEAM; extern const char* LOGGER; + extern const char* OCULUS_STORE; extern const char* TEST; extern const char* TRACING; diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index a768af932e..55eb9c7b85 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -8,21 +8,31 @@ if (WIN32) - # we're using static GLEW, so define GLEW_STATIC - add_definitions(-DGLEW_STATIC) + # we're using static GLEW, so define GLEW_STATIC + add_definitions(-DGLEW_STATIC) - set(TARGET_NAME oculus) - setup_hifi_plugin(Multimedia) - link_hifi_libraries(shared gl gpu gpu-gl controllers ui - plugins ui-plugins display-plugins input-plugins - audio-client networking render-utils) - - include_hifi_library_headers(octree) - - add_dependency_external_projects(LibOVR) - find_package(LibOVR REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${LIBOVR_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${LIBOVR_LIBRARIES}) - target_link_libraries(${TARGET_NAME} Winmm.lib) + # if we were passed an Oculus App ID for entitlement checks, send that along + if (DEFINED ENV{OCULUS_APP_ID}) + add_definitions(-DOCULUS_APP_ID="$ENV{OCULUS_APP_ID}") + endif () + set(TARGET_NAME oculus) + setup_hifi_plugin(Multimedia) + link_hifi_libraries( + shared gl gpu gpu-gl controllers ui + plugins ui-plugins display-plugins input-plugins + audio-client networking render-utils + ) + include_hifi_library_headers(octree) + + add_dependency_external_projects(LibOVR) + find_package(LibOVR REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${LIBOVR_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${LIBOVR_LIBRARIES}) + target_link_libraries(${TARGET_NAME} Winmm.lib) + + add_dependency_external_projects(LibOVRPlatform) + find_package(LibOVRPlatform REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${LIBOVRPLATFORM_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${LIBOVRPLATFORM_LIBRARIES}) endif() diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 8a160b67ff..340b804404 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -15,8 +15,12 @@ #include #include +#define OVRPL_DISABLED +#include + #include #include +#include #include Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") @@ -89,6 +93,18 @@ ovrSession acquireOculusSession() { return session; } +#ifdef OCULUS_APP_ID + if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) { + if (ovr_PlatformInitializeWindows(OCULUS_APP_ID) != ovrPlatformInitialize_Success) { + // we were unable to initialize the platform for entitlement check - fail the check + _quitRequested = true; + } else { + qCDebug(oculus) << "Performing Oculus Platform entitlement check"; + ovr_Entitlement_GetIsViewerEntitled(); + } + } +#endif + Q_ASSERT(0 == refCount); ovrGraphicsLuid luid; if (!OVR_SUCCESS(ovr_Create(&session, &luid))) { @@ -127,6 +143,35 @@ void handleOVREvents() { _quitRequested = status.ShouldQuit; _reorientRequested = status.ShouldRecenter; + + #ifdef OCULUS_APP_ID + + if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) { + // pop messages to see if we got a return for an entitlement check + ovrMessageHandle message = ovr_PopMessage(); + + while (message) { + switch (ovr_Message_GetType(message)) { + case ovrMessage_Entitlement_GetIsViewerEntitled: { + if (!ovr_Message_IsError(message)) { + // this viewer is entitled, no need to flag anything + qCDebug(oculus) << "Oculus Platform entitlement check succeeded, proceeding normally"; + } else { + // we failed the entitlement check, set our flag so the app can stop + qCDebug(oculus) << "Oculus Platform entitlement check failed, app will now quit" << OCULUS_APP_ID; + _quitRequested = true; + } + } + } + + // free the message handle to cleanup and not leak + ovr_FreeMessage(message); + + // pop the next message to check, if there is one + message = ovr_PopMessage(); + } + } +#endif } bool quitRequested() { @@ -217,4 +262,4 @@ controller::Pose ovrControllerPoseToHandPose( pose.velocity = toGlm(handPose.LinearVelocity); pose.valid = true; return pose; -} \ No newline at end of file +} diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 2ed09232e6..57698bd0dc 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -3082,7 +3082,7 @@ var handleHandMessages = function(channel, message, sender) { Messages.messageReceived.connect(handleHandMessages); -var TARGET_UPDATE_HZ = 50; // 50hz good enough (no change in logic) +var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; var lastInterval = Date.now(); @@ -3095,7 +3095,7 @@ var updateTotalWork = 0; var UPDATE_PERFORMANCE_DEBUGGING = false; -var updateIntervalTimer = Script.setInterval(function(){ +function updateWrapper(){ intervalCount++; var thisInterval = Date.now(); @@ -3141,11 +3141,12 @@ var updateIntervalTimer = Script.setInterval(function(){ updateTotalWork = 0; } -}, BASIC_TIMER_INTERVAL_MS); +} +Script.update.connect(updateWrapper); function cleanup() { Menu.removeMenuItem("Developer", "Show Grab Sphere"); - Script.clearInterval(updateIntervalTimer); + Script.update.disconnect(updateWrapper); rightController.cleanup(); leftController.cleanup(); Controller.disableMapping(MAPPING_NAME); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 48f44570fd..2430ce6e87 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -233,6 +233,15 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like removeOverlays(); populateUserList(); break; + case 'updateGain': + data = message.params; + Users.setAvatarGain(data['sessionId'], data['gain']); + break; + case 'displayNameUpdate': + if (MyAvatar.displayName != message.params) { + MyAvatar.displayName = message.params; + } + break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } @@ -448,50 +457,6 @@ triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Cont triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); -// -// Manage the connection between the button and the window. -// -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); -var buttonName = "pal"; -var button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/people.svg"), - visible: true, - hoverState: 2, - defaultState: 1, - buttonState: 1, - alpha: 0.9 -}); -var isWired = false; -function off() { - if (isWired) { // It is not ok to disconnect these twice, hence guard. - Script.update.disconnect(updateOverlays); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - isWired = false; - } - triggerMapping.disable(); // It's ok if we disable twice. - triggerPressMapping.disable(); // see above - removeOverlays(); - Users.requestsDomainListData = false; -} -function onClicked() { - if (!pal.visible) { - Users.requestsDomainListData = true; - populateUserList(); - pal.raise(); - isWired = true; - Script.update.connect(updateOverlays); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - } else { - off(); - } - pal.setVisible(!pal.visible); -} - // // Message from other scripts, such as edit.js // @@ -523,6 +488,7 @@ 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 myData = {}; // we're not includied in ExtendedOverlay.get. +var audioInterval; function getAudioLevel(id) { // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged @@ -555,21 +521,74 @@ function getAudioLevel(id) { return audioLevel; } +function createAudioInterval() { + // we will update the audioLevels periodically + // TODO: tune for efficiency - expecially with large numbers of avatars + return Script.setInterval(function () { + if (pal.visible) { + var param = {}; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + var level = getAudioLevel(id); + // 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); +} -// we will update the audioLevels periodically -// TODO: tune for efficiency - expecially with large numbers of avatars -Script.setInterval(function () { - if (pal.visible) { - var param = {}; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id); - // 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}); +// +// Manage the connection between the button and the window. +// +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var buttonName = "pal"; +var button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/people.svg"), + visible: true, + hoverState: 2, + defaultState: 1, + buttonState: 1, + alpha: 0.9 +}); +var isWired = false; +function off() { + if (isWired) { // It is not ok to disconnect these twice, hence guard. + Script.update.disconnect(updateOverlays); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + isWired = false; } -}, AUDIO_LEVEL_UPDATE_INTERVAL_MS); + triggerMapping.disable(); // It's ok if we disable twice. + triggerPressMapping.disable(); // see above + removeOverlays(); + Users.requestsDomainListData = false; + if (audioInterval) { + Script.clearInterval(audioInterval); + } +} +function onClicked() { + if (!pal.visible) { + Users.requestsDomainListData = true; + populateUserList(); + pal.raise(); + isWired = true; + Script.update.connect(updateOverlays); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); + createAudioInterval(); + } else { + off(); + } + pal.setVisible(!pal.visible); +} +function avatarDisconnected(nodeID) { + // remove from the pal list + pal.sendToQml({method: 'avatarDisconnected', params: [nodeID]}); +} // // Button state. // @@ -582,14 +601,16 @@ button.clicked.connect(onClicked); pal.visibleChanged.connect(onVisibleChanged); pal.closed.connect(off); Users.usernameFromIDReply.connect(usernameFromIDReply); -function clearIgnoredInQMLAndClosePAL() { - pal.sendToQml({ method: 'clearIgnored' }); +Users.avatarDisconnected.connect(avatarDisconnected); + +function clearLocalQMLDataAndClosePAL() { + pal.sendToQml({ method: 'clearLocalQMLData' }); if (pal.visible) { onClicked(); // Close the PAL } } -Window.domainChanged.connect(clearIgnoredInQMLAndClosePAL); -Window.domainConnectionRefused.connect(clearIgnoredInQMLAndClosePAL); +Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); +Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); // // Cleanup. @@ -600,10 +621,11 @@ Script.scriptEnding.connect(function () { pal.visibleChanged.disconnect(onVisibleChanged); pal.closed.disconnect(off); Users.usernameFromIDReply.disconnect(usernameFromIDReply); - Window.domainChanged.disconnect(clearIgnoredInQMLAndClosePAL); - Window.domainConnectionRefused.disconnect(clearIgnoredInQMLAndClosePAL); + Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); + Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); Messages.unsubscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); + Users.avatarDisconnected.disconnect(avatarDisconnected); off(); });