diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index d8d0b10fea..3cacfdedf1 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -50,6 +50,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); + packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); @@ -108,6 +109,13 @@ void AvatarMixer::broadcastAvatarData() { const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; + // only send extra avatar data (avatars out of view, ignored) every Nth AvatarData frame + // Extra avatar data will be sent (AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND/EXTRA_AVATAR_DATA_FRAME_RATIO) times + // per second. + // This value should be a power of two for performance purposes, as the mixer performs a modulo operation every frame + // to determine whether the extra data should be sent. + const int EXTRA_AVATAR_DATA_FRAME_RATIO = 16; + // NOTE: The following code calculates the _performanceThrottlingRatio based on how much the avatar-mixer was // able to sleep. This will eventually be used to ask for an additional avatar-mixer to help out. Currently the value // is unused as it is assumed this should not be hit before the avatar-mixer hits the desired bandwidth limit per client. @@ -205,6 +213,15 @@ void AvatarMixer::broadcastAvatarData() { // use the data rate specifically for avatar data for FRD adjustment checks float avatarDataRateLastSecond = nodeData->getOutboundAvatarDataKbps(); + // When this is true, the AvatarMixer will send Avatar data to a client about avatars that are not in the view frustrum + bool getsOutOfView = nodeData->getRequestsDomainListData(); + + // When this is true, the AvatarMixer will send Avatar data to a client about avatars that they've ignored + bool getsIgnoredByMe = getsOutOfView; + + // When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them + bool getsAnyIgnored = getsIgnoredByMe && node->getCanKick(); + // Check if it is time to adjust what we send this client based on the observed // bandwidth to this node. We do this once a second, which is also the window for // the bandwidth reported by node->getOutboundBandwidth(); @@ -275,14 +292,14 @@ void AvatarMixer::broadcastAvatarData() { // or that has ignored the viewing node if (!otherNode->getLinkedData() || otherNode->getUUID() == node->getUUID() - || node->isIgnoringNodeWithID(otherNode->getUUID()) - || otherNode->isIgnoringNodeWithID(node->getUUID())) { + || (node->isIgnoringNodeWithID(otherNode->getUUID()) && !getsIgnoredByMe) + || (otherNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) { return false; } else { AvatarMixerClientData* otherData = reinterpret_cast(otherNode->getLinkedData()); AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); // Check to see if the space bubble is enabled - if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { + if ((node->isIgnoreRadiusEnabled() && !getsIgnoredByMe) || (otherNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { // Define the minimum bubble size static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); // Define the scale of the box for the current node @@ -333,7 +350,6 @@ void AvatarMixer::broadcastAvatarData() { && (forceSend || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp || distribution(generator) < IDENTITY_SEND_PROBABILITY)) { - sendIdentityPacket(otherNodeData, node); } @@ -349,6 +365,7 @@ void AvatarMixer::broadcastAvatarData() { maxAvatarDistanceThisFrame = std::max(maxAvatarDistanceThisFrame, distanceToAvatar); if (distanceToAvatar != 0.0f + && !getsOutOfView && distribution(generator) > (nodeData->getFullRateDistance() / distanceToAvatar)) { return; } @@ -380,15 +397,21 @@ void AvatarMixer::broadcastAvatarData() { nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), otherNodeData->getLastReceivedSequenceNumber()); - // start a new segment in the PacketList for this avatar - avatarPacketList->startSegment(); - // determine if avatar is in view, to determine how much data to include... glm::vec3 otherNodeBoxScale = (otherNodeData->getPosition() - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f; AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); + bool isInView = nodeData->otherAvatarInView(otherNodeBox); + + // this throttles the extra data to only be sent every Nth message + if (!isInView && getsOutOfView && (lastSeqToReceiver % EXTRA_AVATAR_DATA_FRAME_RATIO > 0)) { + return; + } + + // start a new segment in the PacketList for this avatar + avatarPacketList->startSegment(); AvatarData::AvatarDataDetail detail; - if (!nodeData->otherAvatarInView(otherNodeBox)) { + if (!isInView && !getsOutOfView) { detail = AvatarData::MinimumData; nodeData->incrementAvatarOutOfView(); } else { @@ -527,6 +550,20 @@ void AvatarMixer::handleViewFrustumPacket(QSharedPointer messag } } +void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode) { + auto nodeList = DependencyManager::get(); + nodeList->getOrCreateLinkedData(senderNode); + + if (senderNode->getLinkedData()) { + AvatarMixerClientData* nodeData = dynamic_cast(senderNode->getLinkedData()); + if (nodeData != nullptr) { + bool isRequesting; + message->readPrimitive(&isRequesting); + nodeData->setRequestsDomainListData(isRequesting); + } + } +} + void AvatarMixer::handleAvatarDataPacket(QSharedPointer message, SharedNodePointer senderNode) { auto nodeList = DependencyManager::get(); nodeList->updateNodeWithDataFromPacket(message, senderNode); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 521fe72f39..5d54622ac9 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -42,6 +42,7 @@ private slots: void handleKillAvatarPacket(QSharedPointer message); void handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode); void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 78a30d8206..861086893a 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -101,6 +101,8 @@ public: void incrementAvatarOutOfView() { _recentOtherAvatarsOutOfView++; } const QString& getBaseDisplayName() { return _baseDisplayName; } void setBaseDisplayName(const QString& baseDisplayName) { _baseDisplayName = baseDisplayName; } + bool getRequestsDomainListData() { return _requestsDomainListData; } + void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; } private: AvatarSharedPointer _avatar { new AvatarData() }; @@ -129,6 +131,7 @@ private: int _recentOtherAvatarsInView { 0 }; int _recentOtherAvatarsOutOfView { 0 }; QString _baseDisplayName{}; // The santized key used in determinging unique sessionDisplayName, so that we can remove from dictionary. + bool _requestsDomainListData { false }; }; #endif // hifi_AvatarMixerClientData_h diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 1c98f4e6f3..3dc3069ef5 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/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index aec579755a..d3cbb87e5b 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -72,6 +72,18 @@ Original.CheckBox { border.color: hifi.colors.checkboxCheckedBorder visible: checked && !pressed || !checked && pressed } + + Rectangle { + id: disabledOverlay + visible: !enabled + width: boxSize + height: boxSize + radius: boxRadius + border.width: 1 + border.color: hifi.colors.baseGrayHighlight + color: hifi.colors.baseGrayHighlight + opacity: 0.5 + } } label: Label { diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 9b90ae6c3b..0ad8bfd57e 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -35,6 +35,7 @@ Row { property int usernameTextHeight: 12 property real audioLevel: 0.0 + /* User image commented out for now - will probably be re-introduced later. Column { id: avatarImage // Size @@ -48,10 +49,11 @@ Row { height: parent.height } } + */ Column { id: textContainer // Size - width: parent.width - avatarImage.width - parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing + width: parent.width - /*avatarImage.width - */parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing height: contentHeight // DisplayName Text diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 4abaccfe2c..833cf4efe2 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -40,7 +40,11 @@ Item { property int myCardHeight: 70 property int rowHeight: 70 property int actionButtonWidth: 75 - property int nameCardWidth: width - actionButtonWidth*(iAmAdmin ? 4 : 2) + property int nameCardWidth: width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4 + property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set + 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 // This contains the current user's NameCard and will contain other information in the future Rectangle { @@ -85,7 +89,7 @@ Item { Rectangle { id: adminTab // Size - width: actionButtonWidth * 2 - 2 + width: actionButtonWidth * 2 + 2 height: 40 // Anchors anchors.bottom: myInfo.bottom @@ -169,7 +173,9 @@ Item { movable: false resizable: false } - model: userModel + model: ListModel { + id: userModel + } // This Rectangle refers to each Row in the table. rowDelegate: Rectangle { // The only way I know to specify a row height. @@ -183,16 +189,17 @@ Item { // This Item refers to the contents of each Cell itemDelegate: Item { id: itemCell - property bool isCheckBox: typeof(styleData.value) === 'boolean' + property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore" + property bool isButton: styleData.role === "mute" || styleData.role === "kick" // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { id: nameCard // Properties displayName: styleData.value - userName: model.userName - audioLevel: model.audioLevel - visible: !isCheckBox + userName: model && model.userName + audioLevel: model && model.audioLevel + visible: !isCheckBox && !isButton // Size width: nameCardWidth height: parent.height @@ -200,19 +207,69 @@ Item { anchors.left: parent.left } - // This CheckBox belongs in the columns that contain the action buttons ("Mute", "Ban", etc) + // This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now) + // KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox + // will appear in the "hovered" state. Hovering over the checkbox will fix it. + // Clicking on the sides of the sorting header doesn't cause this problem. + // I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it... + // I'm just going to leave the minor visual bug in. HifiControls.CheckBox { + id: actionCheckBox visible: isCheckBox anchors.centerIn: parent + checked: model[styleData.role] + // If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked. + enabled: !(styleData.role === "personalMute" && model["ignore"]) boxSize: 24 onClicked: { var newValue = !model[styleData.role] - var datum = userData[model.userIndex] - datum[styleData.role] = model[styleData.role] = newValue + userModel.setProperty(model.userIndex, styleData.role, newValue) + userModelData[model.userIndex][styleData.role] = newValue // Defensive programming + Users[styleData.role](model.sessionId, newValue) + if (styleData.role === "ignore") { + userModel.setProperty(model.userIndex, "personalMute", newValue) + userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming + if (newValue) { + ignored[model.sessionId] = userModelData[model.userIndex] + } else { + delete ignored[model.sessionId] + } + } + // http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript + // I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by + // "checked:" statement above. + checked = Qt.binding(function() { return (model[styleData.role])}) + } + } + + // This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now) + HifiControls.Button { + id: actionButton + color: 2 // Red + visible: isButton + anchors.centerIn: parent + width: 32 + height: 24 + onClicked: { Users[styleData.role](model.sessionId) - // Just for now, while we cannot undo things: - userData.splice(model.userIndex, 1) - sortModel() + if (styleData.role === "kick") { + // Just for now, while we cannot undo "Ban": + userModel.remove(model.userIndex) + delete userModelData[model.userIndex] // Defensive programming + sortModel() + } + } + // muted/error glyphs + HiFiGlyphs { + text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted + // Size + size: parent.height*1.3 + // Anchors + anchors.fill: parent + // Style + horizontalAlignment: Text.AlignHCenter + color: enabled ? hifi.buttons.textColor[actionButton.color] + : hifi.buttons.disabledTextColor[actionButton.colorScheme] } } } @@ -256,7 +313,6 @@ Item { onExited: reloadButton.color = (pressed ? hifi.colors.lightGrayText: hifi.colors.darkGray) } } - // Separator between user and admin functions Rectangle { // Size @@ -309,18 +365,22 @@ Item { radius: hifi.dimensions.borderRadius } Rectangle { - width: Math.min(parent.width * 0.75, 400) - height: popupText.contentHeight*2 + width: Math.max(parent.width * 0.75, 400) + height: popupText.contentHeight*1.5 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." + text: "Bold names in the list are Avatar Display Names.\n" + + "If a Display Name isn't set, a unique Session Display Name is assigned." + + "\n\nAdministrators of this domain can also see the Username or Machine ID associated with each avatar present." size: hifi.fontSizes.textFieldInput color: hifi.colors.darkGray horizontalAlignment: Text.AlignHCenter anchors.fill: parent + anchors.leftMargin: 15 + anchors.rightMargin: 15 wrapMode: Text.WordWrap } } @@ -333,11 +393,8 @@ Item { } } - property var userData: [] - 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; + var data = optionalData || userModelData, length = data.length; for (var i = 0; i < length; i++) { if (data[i].sessionId === sessionId) { return i; @@ -349,11 +406,24 @@ Item { switch (message.method) { case 'users': var data = message.params; - var myIndex = findSessionIndex('', data); - iAmAdmin = Users.canKick; - myData = data[myIndex]; - data.splice(myIndex, 1); - userData = data; + var index = -1; + index = findSessionIndex('', data); + if (index !== -1) { + iAmAdmin = Users.canKick; + myData = data[index]; + data.splice(index, 1); + } else { + console.log("This user's data was not found in the user list. PAL will not function properly."); + } + userModelData = data; + for (var ignoredID in ignored) { + index = findSessionIndex(ignoredID); + if (index === -1) { // Add back any missing ignored to the PAL, because they sometimes take a moment to show up. + userModelData.push(ignored[ignoredID]); + } else { // Already appears in PAL; update properties of existing element in model data + userModelData[index] = ignored[ignoredID]; + } + } sortModel(); break; case 'select': @@ -378,11 +448,15 @@ Item { myData.userName = userName; myCard.userName = userName; // Defensive programming } else { - // Get the index in userModel and userData associated with the passed UUID + // Get the index in userModel and userModelData associated with the passed UUID var userIndex = findSessionIndex(userId); - // Set the userName appropriately - userModel.get(userIndex).userName = userName; - userData[userIndex].userName = userName; // Defensive programming + if (userIndex != -1) { + // Set the userName appropriately + userModel.setProperty(userIndex, "userName", userName); + userModelData[userIndex].userName = userName; // Defensive programming + } else { + console.log("updateUsername() called with unknown UUID: ", userId); + } } break; case 'updateAudioLevel': @@ -393,25 +467,28 @@ Item { 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 + if (userIndex != -1) { + userModel.setProperty(userIndex, "audioLevel", audioLevel); + userModelData[userIndex].audioLevel = audioLevel; // Defensive programming + } else { + console.log("updateUsername() called with unknown UUID: ", userId); + } } } break; + case 'clearIgnored': + ignored = {}; + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } } - ListModel { - id: userModel - } function sortModel() { var sortProperty = table.getColumn(table.sortIndicatorColumn).role; var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; var after = -1 * before; - userData.sort(function (a, b) { + userModelData.sort(function (a, b) { var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); switch (true) { case (aValue < bValue): return before; @@ -420,9 +497,10 @@ Item { } }); table.selection.clear(); + userModel.clear(); var userIndex = 0; - userData.forEach(function (datum) { + userModelData.forEach(function (datum) { function init(property) { if (datum[property] === undefined) { datum[property] = false; @@ -437,7 +515,7 @@ Item { function noticeSelection() { var userIds = []; table.selection.forEach(function (userIndex) { - userIds.push(userData[userIndex].sessionId); + userIds.push(userModelData[userIndex].sessionId); }); pal.sendToScript({method: 'selected', params: userIds}); } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ac26bd9017..b58bedb363 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,6 +101,7 @@ #include #include #include +#include #include #include #include @@ -1819,11 +1820,13 @@ void Application::initializeGL() { // Set up the render engine render::CullFunctor cullFunctor = LODManager::shouldRender; _renderEngine->addJob("RenderShadowTask", cullFunctor); + const auto items = _renderEngine->addJob("FetchCullSort", cullFunctor); + assert(items.canCast()); static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; if (QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD)) { - _renderEngine->addJob("RenderForwardTask", cullFunctor); + _renderEngine->addJob("RenderForwardTask", items.get()); } else { - _renderEngine->addJob("RenderDeferredTask", cullFunctor); + _renderEngine->addJob("RenderDeferredTask", items.get()); } _renderEngine->load(); _renderEngine->registerScene(_main3DScene); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index dc5b6233aa..1ade21930d 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -44,6 +44,7 @@ #include "Util.h" #include "world.h" #include "InterfaceLogging.h" +#include "SceneScriptingInterface.h" #include "SoftAttachmentModel.h" #include @@ -292,7 +293,10 @@ void Avatar::updateAvatarEntities() { } } - +void Avatar::setShouldDie() { + // This will cause the avatar to be shrunk away and removed (the actual Avatar gets removed), but then it comes back. + _owningAvatarMixer.clear(); +} void Avatar::simulate(float deltaTime) { PerformanceTimer perfTimer("simulate"); @@ -459,6 +463,7 @@ bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr attachmentModel->addToScene(scene, pendingChanges); } + _inScene = true; return true; } @@ -469,6 +474,7 @@ void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptrremoveFromScene(scene, pendingChanges); } + _inScene = false; } void Avatar::updateRenderItem(render::PendingChanges& pendingChanges) { @@ -1328,3 +1334,21 @@ void Avatar::setParentJointIndex(quint16 parentJointIndex) { } } } + +void Avatar::addToScene(AvatarSharedPointer myHandle) { + render::ScenePointer scene = qApp->getMain3DScene(); + if (scene) { + render::PendingChanges pendingChanges; + if (DependencyManager::get()->shouldRenderAvatars() && !DependencyManager::get()->isIgnoringNode(getSessionUUID())) { + addToScene(myHandle, scene, pendingChanges); + } + scene->enqueuePendingChanges(pendingChanges); + } else { + qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown"; + } +} +void Avatar::ensureInScene(AvatarSharedPointer self) { + if (!_inScene) { + addToScene(self); + } +} \ No newline at end of file diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 784f380028..d03a8e9a54 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -178,6 +178,8 @@ public: glm::vec3 getUncachedRightPalmPosition() const; glm::quat getUncachedRightPalmRotation() const; + Q_INVOKABLE void setShouldDie(); + public slots: // FIXME - these should be migrated to use Pose data instead @@ -254,6 +256,9 @@ protected: ThreadSafeValueCache _rightPalmPositionCache { glm::vec3() }; ThreadSafeValueCache _rightPalmRotationCache { glm::quat() }; + void addToScene(AvatarSharedPointer self); + void ensureInScene(AvatarSharedPointer self); + private: int _leftPointerGeometryID { 0 }; int _rightPointerGeometryID { 0 }; @@ -262,6 +267,7 @@ private: bool _shouldAnimate { true }; bool _shouldSkipRender { false }; bool _isLookAtTarget { false }; + bool _inScene { false }; float getBoundingRadius() const; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c5222641ff..e3ccc10a65 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -82,7 +82,11 @@ AvatarManager::AvatarManager(QObject* parent) : // when we hear that the user has ignored an avatar by session UUID // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer - connect(nodeList.data(), "ignoredNode", this, "removeAvatar"); + connect(nodeList.data(), &NodeList::ignoredNode, this, [=](const QUuid& nodeID, bool enabled) { + if (enabled) { + removeAvatar(nodeID); + } + }); } AvatarManager::~AvatarManager() { @@ -159,6 +163,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { removeAvatar(avatarIterator.key()); ++avatarIterator; } else { + avatar->ensureInScene(avatar); avatar->simulate(deltaTime); ++avatarIterator; @@ -219,16 +224,7 @@ AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWe auto newAvatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer); auto rawRenderableAvatar = std::static_pointer_cast(newAvatar); - render::ScenePointer scene = qApp->getMain3DScene(); - if (scene) { - render::PendingChanges pendingChanges; - if (DependencyManager::get()->shouldRenderAvatars()) { - rawRenderableAvatar->addToScene(rawRenderableAvatar, scene, pendingChanges); - } - scene->enqueuePendingChanges(pendingChanges); - } else { - qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown"; - } + rawRenderableAvatar->addToScene(rawRenderableAvatar); return newAvatar; } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index f38796ca08..a423e34f8f 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -93,7 +93,6 @@ private: // virtual overrides virtual AvatarSharedPointer newSharedAvatar() override; virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) override; - virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override; QVector _avatarFades; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 8871769261..c708176da7 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -112,7 +112,7 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer mess // make sure this isn't our own avatar data or for a previously ignored node auto nodeList = DependencyManager::get(); - if (sessionUUID != _lastOwnerSessionUUID && !nodeList->isIgnoringNode(sessionUUID)) { + if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) { auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); // have the matching (or new) avatar parse the data from the packet @@ -145,7 +145,7 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer identity.uuid = EMPTY; } } - if (!nodeList->isIgnoringNode(identity.uuid)) { + if (!nodeList->isIgnoringNode(identity.uuid) || nodeList->getRequestsDomainListData()) { // mesh URL for a UUID, find avatar in our list auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); avatar->processAvatarIdentity(identity); diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 66086e583e..8063075e22 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -81,17 +81,24 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } -void Node::parseIgnoreRequestMessage(QSharedPointer message) { +void Node::parseIgnoreRequestMessage(QSharedPointer message) { + bool addToIgnore; + message->readPrimitive(&addToIgnore); while (message->getBytesLeftToRead()) { // parse out the UUID being ignored from the packet QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - addIgnoredNode(ignoredUUID); + if (addToIgnore) { + addIgnoredNode(ignoredUUID); + } else { + removeIgnoredNode(ignoredUUID); + } } } void Node::addIgnoredNode(const QUuid& otherNodeID) { if (!otherNodeID.isNull() && otherNodeID != _uuid) { + QReadLocker lock { &_ignoredNodeIDSetLock }; qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for" << uuidStringWithoutCurlyBraces(_uuid); @@ -102,6 +109,20 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) { } } +void Node::removeIgnoredNode(const QUuid& otherNodeID) { + if (!otherNodeID.isNull() && otherNodeID != _uuid) { + // insert/find are read locked concurrently. unsafe_erase is not concurrent, and needs a write lock. + QWriteLocker lock { &_ignoredNodeIDSetLock }; + qCDebug(networking) << "Removing" << uuidStringWithoutCurlyBraces(otherNodeID) << "from ignore set for" + << uuidStringWithoutCurlyBraces(_uuid); + + // remove the session UUID from the set of ignored ones for this listening node + _ignoredNodeIDSet.unsafe_erase(otherNodeID); + } else { + qCWarning(networking) << "Node::removeIgnoredNode called with null ID or ID of ignoring node."; + } +} + void Node::parseIgnoreRadiusRequestMessage(QSharedPointer message) { bool enabled; message->readPrimitive(&enabled); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 2a64bb9943..28afb8b943 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -73,7 +74,8 @@ public: void parseIgnoreRequestMessage(QSharedPointer message); void addIgnoredNode(const QUuid& otherNodeID); - bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } + void removeIgnoredNode(const QUuid& otherNodeID); + bool isIgnoringNodeWithID(const QUuid& nodeID) const { QReadLocker lock { &_ignoredNodeIDSetLock }; return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } void parseIgnoreRadiusRequestMessage(QSharedPointer message); friend QDataStream& operator<<(QDataStream& out, const Node& node); @@ -97,6 +99,7 @@ private: MovingPercentile _clockSkewMovingPercentile; NodePermissions _permissions; tbb::concurrent_unordered_set _ignoredNodeIDSet; + mutable QReadWriteLock _ignoredNodeIDSetLock; std::atomic_bool _ignoreRadiusEnabled; }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 0f18dd1b55..bd3203150e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -242,6 +242,10 @@ void NodeList::reset() { _ignoredSetLock.lockForWrite(); _ignoredNodeIDs.clear(); _ignoredSetLock.unlock(); + // lock and clear our set of personally muted IDs + _personalMutedSetLock.lockForWrite(); + _personalMutedNodeIDs.clear(); + _personalMutedSetLock.unlock(); // refresh the owner UUID to the NULL UUID setSessionUUID(QUuid()); @@ -777,7 +781,7 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN sendPacket(std::move(ignorePacket), *destinationNode); } -void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { +void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it if (!nodeID.isNull() && _sessionUUID != nodeID) { eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { @@ -786,25 +790,36 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { } else { return false; } - }, [&nodeID, this](const SharedNodePointer& destinationNode) { + }, [&nodeID, ignoreEnabled, this](const SharedNodePointer& destinationNode) { // create a reliable NLPacket with space for the ignore UUID - auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); + auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID + sizeof(bool), true); + ignorePacket->writePrimitive(ignoreEnabled); // write the node ID to the packet ignorePacket->write(nodeID.toRfc4122()); - qCDebug(networking) << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); + qCDebug(networking) << "Sending packet to" << (destinationNode->getType() == NodeType::AudioMixer ? "AudioMixer" : "AvatarMixer") << "to" + << (ignoreEnabled ? "ignore" : "unignore") << "node" << uuidStringWithoutCurlyBraces(nodeID); // send off this ignore packet reliably to the matching node sendPacket(std::move(ignorePacket), *destinationNode); }); - QReadLocker setLocker { &_ignoredSetLock }; - - // add this nodeID to our set of ignored IDs - _ignoredNodeIDs.insert(nodeID); - - emit ignoredNode(nodeID); + if (ignoreEnabled) { + QReadLocker ignoredSetLocker{ &_ignoredSetLock }; // read lock for insert + QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert + // add this nodeID to our set of ignored IDs + _ignoredNodeIDs.insert(nodeID); + // add this nodeID to our set of personal muted IDs + _personalMutedNodeIDs.insert(nodeID); + emit ignoredNode(nodeID, true); + } else { + QWriteLocker ignoredSetLocker{ &_ignoredSetLock }; // write lock for unsafe_erase + QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; // write lock for unsafe_erase + _ignoredNodeIDs.unsafe_erase(nodeID); + _personalMutedNodeIDs.unsafe_erase(nodeID); + emit ignoredNode(nodeID, false); + } } else { qWarning() << "NodeList::ignoreNodeBySessionID called with an invalid ID or an ID which matches the current session ID."; @@ -812,21 +827,94 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { } bool NodeList::isIgnoringNode(const QUuid& nodeID) const { - QReadLocker setLocker { &_ignoredSetLock }; + QReadLocker ignoredSetLocker{ &_ignoredSetLock }; return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend(); } +void NodeList::personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled) { + // cannot personal mute yourself, or nobody + if (!nodeID.isNull() && _sessionUUID != nodeID) { + auto audioMixer = soloNodeOfType(NodeType::AudioMixer); + if (audioMixer) { + if (isIgnoringNode(nodeID)) { + qCDebug(networking) << "You can't personally mute or unmute a node you're already ignoring."; + } + else { + // setup the packet + auto personalMutePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID + sizeof(bool), true); + + personalMutePacket->writePrimitive(muteEnabled); + // write the node ID to the packet + personalMutePacket->write(nodeID.toRfc4122()); + + qCDebug(networking) << "Sending Personal Mute Packet to" << (muteEnabled ? "mute" : "unmute") << "node" << uuidStringWithoutCurlyBraces(nodeID); + + sendPacket(std::move(personalMutePacket), *audioMixer); + + + if (muteEnabled) { + QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert + // add this nodeID to our set of personal muted IDs + _personalMutedNodeIDs.insert(nodeID); + } else { + QWriteLocker personalMutedSetLocker{ &_personalMutedSetLock }; // write lock for unsafe_erase + _personalMutedNodeIDs.unsafe_erase(nodeID); + } + } + } else { + qWarning() << "Couldn't find audio mixer to send node personal mute request"; + } + } else { + qWarning() << "NodeList::personalMuteNodeBySessionID called with an invalid ID or an ID which matches the current session ID."; + } +} + +bool NodeList::isPersonalMutingNode(const QUuid& nodeID) const { + QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; + return _personalMutedNodeIDs.find(nodeID) != _personalMutedNodeIDs.cend(); +} + void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { - if (newNode->getType() == NodeType::AudioMixer || newNode->getType() == NodeType::AvatarMixer) { + if (newNode->getType() == NodeType::AudioMixer) { // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, // so send that list along now (assuming it isn't empty) - QReadLocker setLocker { &_ignoredSetLock }; + QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; + + if (_personalMutedNodeIDs.size() > 0) { + // setup a packet list so we can send the stream of ignore IDs + auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + + // Force the "enabled" flag in this packet to true + personalMutePacketList->writePrimitive(true); + + // enumerate the ignored IDs and write them to the packet list + auto it = _personalMutedNodeIDs.cbegin(); + while (it != _personalMutedNodeIDs.end()) { + personalMutePacketList->write(it->toRfc4122()); + ++it; + } + + // send this NLPacketList to the new node + sendPacketList(std::move(personalMutePacketList), *newNode); + } + + // also send them the current ignore radius state. + sendIgnoreRadiusStateToNode(newNode); + } + if (newNode->getType() == NodeType::AvatarMixer) { + // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, + // so send that list along now (assuming it isn't empty) + + QReadLocker ignoredSetLocker{ &_ignoredSetLock }; if (_ignoredNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + // Force the "enabled" flag in this packet to true + ignorePacketList->writePrimitive(true); + // enumerate the ignored IDs and write them to the packet list auto it = _ignoredNodeIDs.cbegin(); while (it != _ignoredNodeIDs.end()) { @@ -930,3 +1018,18 @@ void NodeList::processUsernameFromIDReply(QSharedPointer messag emit usernameFromIDReply(nodeUUIDString, username, machineFingerprintString); } + +void NodeList::setRequestsDomainListData(bool isRequesting) { + // Tell the avatar mixer whether I want to receive any additional data to which I might be entitled + if (_requestsDomainListData == isRequesting) { + return; + } + eachMatchingNode([](const SharedNodePointer& node)->bool { + return node->getType() == NodeType::AvatarMixer; + }, [this, isRequesting](const SharedNodePointer& destinationNode) { + auto packet = NLPacket::create(PacketType::RequestsDomainListData, sizeof(bool), true); // reliable + packet->writePrimitive(isRequesting); + sendPacket(std::move(packet), *destinationNode); + }); + _requestsDomainListData = isRequesting; +} diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index d3f04cedd8..75958f1847 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -76,12 +76,16 @@ public: void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); } void enableIgnoreRadius() { ignoreNodesInRadius(true); } void disableIgnoreRadius() { ignoreNodesInRadius(false); } - void ignoreNodeBySessionID(const QUuid& nodeID); + void ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled); bool isIgnoringNode(const QUuid& nodeID) const; + void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); + bool isPersonalMutingNode(const QUuid& nodeID) const; void kickNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID); void requestUsernameFromSessionID(const QUuid& nodeID); + bool getRequestsDomainListData() { return _requestsDomainListData; } + void setRequestsDomainListData(bool isRequesting); public slots: void reset(); @@ -109,7 +113,7 @@ public slots: signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); - void ignoredNode(const QUuid& nodeID); + void ignoredNode(const QUuid& nodeID, bool enabled); void ignoreRadiusEnabledChanged(bool isIgnored); void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); @@ -153,9 +157,12 @@ private: HifiSockAddr _assignmentServerSocket; bool _isShuttingDown { false }; QTimer _keepAlivePingTimer; + bool _requestsDomainListData; mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; + mutable QReadWriteLock _personalMutedSetLock; + tbb::concurrent_unordered_set _personalMutedNodeIDs; void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); Setting::Handle _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 8c43aa2bc4..07e1ece0cb 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -53,7 +53,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::SessionDisplayName); + return static_cast(AvatarMixerPacketVersion::Unignore); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetGetInfo: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 162e565b83..1867d2193c 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -104,7 +104,8 @@ public: UsernameFromIDRequest, UsernameFromIDReply, ViewFrustum, - LAST_PACKET_TYPE = ViewFrustum + RequestsDomainListData, + LAST_PACKET_TYPE = RequestsDomainListData }; }; @@ -207,7 +208,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { SensorToWorldMat, HandControllerJoints, HasKillAvatarReason, - SessionDisplayName + SessionDisplayName, + Unignore }; enum class DomainConnectRequestVersion : PacketVersion { @@ -242,6 +244,7 @@ enum class AudioVersion : PacketVersion { Exactly10msAudioPackets, TerminatingStreamStats, SpaceBubbleChanges, + HasPersonalMute, }; #endif // hifi_PacketHeaders_h diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d8f3b325f1..f8cd112007 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -48,49 +48,19 @@ using namespace render; extern void initOverlay3DPipelines(render::ShapePlumber& plumber); extern void initDeferredPipelines(render::ShapePlumber& plumber); -RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { - cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; }; - +RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { // Prepare the ShapePipelines ShapePlumberPointer shapePlumber = std::make_shared(); initDeferredPipelines(*shapePlumber); - // CPU jobs: - // Fetch and cull the items from the scene - auto spatialFilter = ItemFilter::Builder::visibleWorldItems().withoutLayered(); - const auto spatialSelection = addJob("FetchSceneSelection", spatialFilter); - const auto culledSpatialSelection = addJob("CullSceneSelection", spatialSelection, cullFunctor, RenderDetails::ITEM, spatialFilter); - - // Overlays are not culled - const auto nonspatialSelection = addJob("FetchOverlaySelection"); - - // Multi filter visible items into different buckets - const int NUM_FILTERS = 3; - const int OPAQUE_SHAPE_BUCKET = 0; - const int TRANSPARENT_SHAPE_BUCKET = 1; - const int LIGHT_BUCKET = 2; - const int BACKGROUND_BUCKET = 2; - MultiFilterItem::ItemFilterArray spatialFilters = { { - ItemFilter::Builder::opaqueShape(), - ItemFilter::Builder::transparentShape(), - ItemFilter::Builder::light() - } }; - MultiFilterItem::ItemFilterArray nonspatialFilters = { { - ItemFilter::Builder::opaqueShape(), - ItemFilter::Builder::transparentShape(), - ItemFilter::Builder::background() - } }; - const auto filteredSpatialBuckets = addJob>("FilterSceneSelection", culledSpatialSelection, spatialFilters).get::ItemBoundsArray>(); - const auto filteredNonspatialBuckets = addJob>("FilterOverlaySelection", nonspatialSelection, nonspatialFilters).get::ItemBoundsArray>(); - - // Extract / Sort opaques / Transparents / Lights / Overlays - const auto opaques = addJob("DepthSortOpaque", filteredSpatialBuckets[OPAQUE_SHAPE_BUCKET]); - const auto transparents = addJob("DepthSortTransparent", filteredSpatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false)); - const auto lights = filteredSpatialBuckets[LIGHT_BUCKET]; - - const auto overlayOpaques = addJob("DepthSortOverlayOpaque", filteredNonspatialBuckets[OPAQUE_SHAPE_BUCKET]); - const auto overlayTransparents = addJob("DepthSortOverlayTransparent", filteredNonspatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false)); - const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET]; + // Extract opaques / transparents / lights / overlays + const auto opaques = items[0]; + const auto transparents = items[1]; + const auto lights = items[2]; + const auto overlayOpaques = items[3]; + const auto overlayTransparents = items[4]; + const auto background = items[5]; + const auto spatialSelection = items[6]; // Prepare deferred, generate the shared Deferred Frame Transform const auto deferredFrameTransform = addJob("DeferredFrameTransform"); @@ -200,7 +170,7 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { addJob("DebugAmbientOcclusion", debugAmbientOcclusionInputs); - // Scene Octree Debuging job + // Scene Octree Debugging job { addJob("DrawSceneOctree", spatialSelection); addJob("DrawItemSelection", spatialSelection); @@ -228,26 +198,6 @@ RenderDeferredTask::RenderDeferredTask(CullFunctor cullFunctor) { } -void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { - // sanity checks - assert(sceneContext); - if (!sceneContext->_scene) { - return; - } - - - // Is it possible that we render without a viewFrustum ? - if (!(renderContext->args && renderContext->args->hasViewFrustum())) { - return; - } - - auto config = std::static_pointer_cast(renderContext->jobConfig); - - for (auto job : _jobs) { - job.run(sceneContext, renderContext); - } -} - void BeginGPURangeTimer::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer) { timer = _gpuTimer; gpu::doInBatch(renderContext->args->_context, [&](gpu::Batch& batch) { diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index fb15e34569..8a95447e67 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -13,18 +13,18 @@ #define hifi_RenderDeferredTask_h #include -#include +#include #include "LightingModel.h" class BeginGPURangeTimer { public: using JobModel = render::Job::ModelO; - + BeginGPURangeTimer(const std::string& name) : _gpuTimer(std::make_shared(name)) {} - + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer); - + protected: gpu::RangeTimerPointer _gpuTimer; }; @@ -35,12 +35,12 @@ class EndGPURangeTimer { public: using Config = GPURangeTimerConfig; using JobModel = render::Job::ModelI; - + EndGPURangeTimer() {} - + void configure(const Config& config) {} void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::RangeTimerPointer& timer); - + protected: }; @@ -192,20 +192,11 @@ public: void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer); }; -using RenderDeferredTaskConfig = render::GPUTaskConfig; - class RenderDeferredTask : public render::Task { public: - using Config = RenderDeferredTaskConfig; - RenderDeferredTask(render::CullFunctor cullFunctor); + using JobModel = Model; - void configure(const Config& config) {} - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - - using JobModel = Model; - -protected: - gpu::RangeTimerPointer _gpuTimer; + RenderDeferredTask(RenderFetchCullSortTask::Output items); }; #endif // hifi_RenderDeferredTask_h diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index ed4f3d27e3..e78d457666 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -19,9 +19,6 @@ #include #include -#include -#include - #include "FramebufferCache.h" #include "TextureCache.h" @@ -32,45 +29,14 @@ using namespace render; -RenderForwardTask::RenderForwardTask(CullFunctor cullFunctor) { - // CPU jobs: - // Fetch and cull the items from the scene - const auto spatialSelection = addJob("FetchSceneSelection"); - - cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; }; - auto spatialFilter = ItemFilter::Builder::visibleWorldItems().withoutLayered(); - const auto culledSpatialSelection = addJob("CullSceneSelection", spatialSelection, cullFunctor, RenderDetails::ITEM, spatialFilter); - - // Overlays are not culled - const auto nonspatialSelection = addJob("FetchOverlaySelection"); - - // Multi filter visible items into different buckets - const int NUM_FILTERS = 3; - const int OPAQUE_SHAPE_BUCKET = 0; - const int TRANSPARENT_SHAPE_BUCKET = 1; - const int LIGHT_BUCKET = 2; - const int BACKGROUND_BUCKET = 2; - MultiFilterItem::ItemFilterArray spatialFilters = { { - ItemFilter::Builder::opaqueShape(), - ItemFilter::Builder::transparentShape(), - ItemFilter::Builder::light() - } }; - MultiFilterItem::ItemFilterArray nonspatialFilters = { { - ItemFilter::Builder::opaqueShape(), - ItemFilter::Builder::transparentShape(), - ItemFilter::Builder::background() - } }; - const auto filteredSpatialBuckets = addJob>("FilterSceneSelection", culledSpatialSelection, spatialFilters).get::ItemBoundsArray>(); - const auto filteredNonspatialBuckets = addJob>("FilterOverlaySelection", nonspatialSelection, nonspatialFilters).get::ItemBoundsArray>(); - - // Extract / Sort opaques / Transparents / Lights / Overlays - const auto opaques = addJob("DepthSortOpaque", filteredSpatialBuckets[OPAQUE_SHAPE_BUCKET]); - const auto transparents = addJob("DepthSortTransparent", filteredSpatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false)); - const auto lights = filteredSpatialBuckets[LIGHT_BUCKET]; - - const auto overlayOpaques = addJob("DepthSortOverlayOpaque", filteredNonspatialBuckets[OPAQUE_SHAPE_BUCKET]); - const auto overlayTransparents = addJob("DepthSortOverlayTransparent", filteredNonspatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false)); - const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET]; +RenderForwardTask::RenderForwardTask(RenderFetchCullSortTask::Output items) { + // Extract opaques / transparents / lights / overlays + const auto opaques = items[0]; + const auto transparents = items[1]; + const auto lights = items[2]; + const auto overlayOpaques = items[3]; + const auto overlayTransparents = items[4]; + const auto background = items[5]; const auto framebuffer = addJob("PrepareFramebuffer"); @@ -83,26 +49,6 @@ RenderForwardTask::RenderForwardTask(CullFunctor cullFunctor) { addJob("Blit", framebuffer); } -void RenderForwardTask::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { - // sanity checks - assert(sceneContext); - if (!sceneContext->_scene) { - return; - } - - - // Is it possible that we render without a viewFrustum ? - if (!(renderContext->args && renderContext->args->hasViewFrustum())) { - return; - } - - auto config = std::static_pointer_cast(renderContext->jobConfig); - - for (auto job : _jobs) { - job.run(sceneContext, renderContext); - } -} - void PrepareFramebuffer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, gpu::FramebufferPointer& framebuffer) { auto framebufferCache = DependencyManager::get(); auto framebufferSize = framebufferCache->getFrameBufferSize(); diff --git a/libraries/render-utils/src/RenderForwardTask.h b/libraries/render-utils/src/RenderForwardTask.h index 90d2ceb79c..000210c761 100755 --- a/libraries/render-utils/src/RenderForwardTask.h +++ b/libraries/render-utils/src/RenderForwardTask.h @@ -13,20 +13,14 @@ #define hifi_RenderForwardTask_h #include -#include +#include #include "LightingModel.h" -using RenderForwardTaskConfig = render::GPUTaskConfig; - class RenderForwardTask : public render::Task { public: - using Config = RenderForwardTaskConfig; - RenderForwardTask(render::CullFunctor cullFunctor); + using JobModel = Model; - void configure(const Config& config) {} - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); - - using JobModel = Model; + RenderForwardTask(RenderFetchCullSortTask::Output items); }; class PrepareFramebuffer { diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index aed360823d..eb46b8f46e 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -115,6 +115,8 @@ RenderShadowTask::RenderShadowTask(CullFunctor cullFunctor) { skinProgram, state); } + const auto cachedMode = addJob("Setup"); + // CPU jobs: // Fetch and cull the items from the scene auto shadowFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered(); @@ -127,6 +129,8 @@ RenderShadowTask::RenderShadowTask(CullFunctor cullFunctor) { // GPU jobs: Render to shadow map addJob("RenderShadowMap", sortedShapes, shapePlumber); + + addJob("Teardown", cachedMode); } void RenderShadowTask::configure(const Config& configuration) { @@ -135,25 +139,13 @@ void RenderShadowTask::configure(const Config& configuration) { Task::configure(configuration); } -void RenderShadowTask::run(const SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { - assert(sceneContext); - RenderArgs* args = renderContext->args; - - // sanity checks - if (!sceneContext->_scene || !args) { - return; - } - +void RenderShadowSetup::run(const SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, Output& output) { auto lightStage = DependencyManager::get()->getLightStage(); const auto globalShadow = lightStage->getShadow(0); - // If the global light is not set, bail - if (!globalShadow) { - return; - } - // Cache old render args - RenderArgs::RenderMode mode = args->_renderMode; + RenderArgs* args = renderContext->args; + output = args->_renderMode; auto nearClip = args->getViewFrustum().getNearClip(); float nearDepth = -args->_boomOffset.z; @@ -163,14 +155,12 @@ void RenderShadowTask::run(const SceneContextPointer& sceneContext, const render // Set the keylight render args args->pushViewFrustum(*(globalShadow->getFrustum())); args->_renderMode = RenderArgs::SHADOW_RENDER_MODE; +} - // TODO: Allow runtime manipulation of culling ShouldRenderFunctor - - for (auto job : _jobs) { - job.run(sceneContext, renderContext); - } +void RenderShadowTeardown::run(const SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Input& input) { + RenderArgs* args = renderContext->args; // Reset the render args args->popViewFrustum(); - args->_renderMode = mode; + args->_renderMode = input; }; diff --git a/libraries/render-utils/src/RenderShadowTask.h b/libraries/render-utils/src/RenderShadowTask.h index 4a6ff1b0e9..d2e58f8362 100644 --- a/libraries/render-utils/src/RenderShadowTask.h +++ b/libraries/render-utils/src/RenderShadowTask.h @@ -49,7 +49,20 @@ public: RenderShadowTask(render::CullFunctor shouldRender); void configure(const Config& configuration); - void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); +}; + +class RenderShadowSetup { +public: + using Output = RenderArgs::RenderMode; + using JobModel = render::Job::ModelO; + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, Output& output); +}; + +class RenderShadowTeardown { +public: + using Input = RenderArgs::RenderMode; + using JobModel = render::Job::ModelI; + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const Input& input); }; #endif // hifi_RenderShadowTask_h diff --git a/libraries/render/src/render/Engine.cpp b/libraries/render/src/render/Engine.cpp index 3c9f2b643a..970cc142e0 100644 --- a/libraries/render/src/render/Engine.cpp +++ b/libraries/render/src/render/Engine.cpp @@ -52,11 +52,3 @@ void Engine::load() { } } } - -void Engine::run() { - for (auto job : _jobs) { - job.run(_sceneContext, _renderContext); - } - -} - diff --git a/libraries/render/src/render/Engine.h b/libraries/render/src/render/Engine.h index d2bb42e5ff..de8340c33e 100644 --- a/libraries/render/src/render/Engine.h +++ b/libraries/render/src/render/Engine.h @@ -39,8 +39,8 @@ namespace render { RenderContextPointer getRenderContext() const { return _renderContext; } // Render a frame - // A frame must have a scene registered and a context set to render - void run(); + // Must have a scene registered and a context set + void run() { assert(_sceneContext && _renderContext); Task::run(_sceneContext, _renderContext); } protected: SceneContextPointer _sceneContext; diff --git a/libraries/render/src/render/RenderFetchCullSortTask.cpp b/libraries/render/src/render/RenderFetchCullSortTask.cpp new file mode 100644 index 0000000000..3195d8c5f8 --- /dev/null +++ b/libraries/render/src/render/RenderFetchCullSortTask.cpp @@ -0,0 +1,61 @@ +// +// RenderFetchCullSortTask.cpp +// render/src/ +// +// Created by Zach Pomerantz on 12/22/2016. +// 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 +// + +#include "RenderFetchCullSortTask.h" + +#include "CullTask.h" +#include "SortTask.h" + +using namespace render; + +RenderFetchCullSortTask::RenderFetchCullSortTask(CullFunctor cullFunctor) { + cullFunctor = cullFunctor ? cullFunctor : [](const RenderArgs*, const AABox&){ return true; }; + + // CPU jobs: + // Fetch and cull the items from the scene + auto spatialFilter = ItemFilter::Builder::visibleWorldItems().withoutLayered(); + const auto spatialSelection = addJob("FetchSceneSelection", spatialFilter); + const auto culledSpatialSelection = addJob("CullSceneSelection", spatialSelection, cullFunctor, RenderDetails::ITEM, spatialFilter); + + // Overlays are not culled + const auto nonspatialSelection = addJob("FetchOverlaySelection"); + + // Multi filter visible items into different buckets + const int NUM_FILTERS = 3; + const int OPAQUE_SHAPE_BUCKET = 0; + const int TRANSPARENT_SHAPE_BUCKET = 1; + const int LIGHT_BUCKET = 2; + const int BACKGROUND_BUCKET = 2; + MultiFilterItem::ItemFilterArray spatialFilters = { { + ItemFilter::Builder::opaqueShape(), + ItemFilter::Builder::transparentShape(), + ItemFilter::Builder::light() + } }; + MultiFilterItem::ItemFilterArray nonspatialFilters = { { + ItemFilter::Builder::opaqueShape(), + ItemFilter::Builder::transparentShape(), + ItemFilter::Builder::background() + } }; + const auto filteredSpatialBuckets = addJob>("FilterSceneSelection", culledSpatialSelection, spatialFilters).get::ItemBoundsArray>(); + const auto filteredNonspatialBuckets = addJob>("FilterOverlaySelection", nonspatialSelection, nonspatialFilters).get::ItemBoundsArray>(); + + // Extract opaques / transparents / lights / overlays + const auto opaques = addJob("DepthSortOpaque", filteredSpatialBuckets[OPAQUE_SHAPE_BUCKET]); + const auto transparents = addJob("DepthSortTransparent", filteredSpatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false)); + const auto lights = filteredSpatialBuckets[LIGHT_BUCKET]; + + const auto overlayOpaques = addJob("DepthSortOverlayOpaque", filteredNonspatialBuckets[OPAQUE_SHAPE_BUCKET]); + const auto overlayTransparents = addJob("DepthSortOverlayTransparent", filteredNonspatialBuckets[TRANSPARENT_SHAPE_BUCKET], DepthSortItems(false)); + const auto background = filteredNonspatialBuckets[BACKGROUND_BUCKET]; + + setOutput(Output{{ + opaques, transparents, lights, overlayOpaques, overlayTransparents, background, spatialSelection }}); +} diff --git a/libraries/render/src/render/RenderFetchCullSortTask.h b/libraries/render/src/render/RenderFetchCullSortTask.h new file mode 100644 index 0000000000..bea45247b0 --- /dev/null +++ b/libraries/render/src/render/RenderFetchCullSortTask.h @@ -0,0 +1,28 @@ +// +// RenderFetchCullSortTask.h +// render/src/ +// +// Created by Zach Pomerantz on 12/22/2016. +// 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 +// + +#ifndef hifi_RenderFetchCullSortTask_h +#define hifi_RenderFetchCullSortTask_h + +#include + +#include "Task.h" +#include "CullTask.h" + +class RenderFetchCullSortTask : public render::Task { +public: + using Output = std::array; + using JobModel = ModelO; + + RenderFetchCullSortTask(render::CullFunctor cullFunctor); +}; + +#endif // hifi_RenderFetchCullSortTask_h diff --git a/libraries/render/src/render/Task.h b/libraries/render/src/render/Task.h index 68643110b8..49273d79a5 100644 --- a/libraries/render/src/render/Task.h +++ b/libraries/render/src/render/Task.h @@ -45,8 +45,9 @@ public: } template Varying(const T& data) : _concept(std::make_shared>(data)) {} - template T& edit() { return std::static_pointer_cast>(_concept)->_data; } + template bool canCast() const { return !!std::dynamic_pointer_cast>(_concept); } template const T& get() const { return std::static_pointer_cast>(_concept)->_data; } + template T& edit() { return std::static_pointer_cast>(_concept)->_data; } // access potential sub varyings contained in this one. @@ -440,6 +441,9 @@ template void jobConfigure(T& data, const C& configuration) { template void jobConfigure(T&, const JobConfig&) { // nop, as the default JobConfig was used, so the data does not need a configure method } +template void jobConfigure(T&, const TaskConfig&) { + // nop, as the default TaskConfig was used, so the data does not need a configure method +} template void jobRun(T& data, const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const JobNoIO& input, JobNoIO& output) { data.run(sceneContext, renderContext); } @@ -591,24 +595,20 @@ class Task { public: using Config = TaskConfig; using QConfigPointer = Job::QConfigPointer; - using None = Job::None; - template class Model : public Job::Concept { + template class Model : public Job::Concept { public: using Data = T; - using Input = I; - using Output = O; + using Config = C; + using Input = Job::None; Data _data; - Varying _input; - Varying _output; - const Varying getInput() const override { return _input; } - const Varying getOutput() const override { return _output; } + const Varying getOutput() const override { return _data._output; } template Model(const Varying& input, A&&... args) : - Concept(nullptr), _data(Data(std::forward(args)...)), _input(input), _output(Output()) { + Concept(nullptr), _data(Data(std::forward(args)...)) { // Recreate the Config to use the templated type _data.template createConfiguration(); _config = _data.getConfiguration(); @@ -620,16 +620,15 @@ public: } void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) override { - renderContext->jobConfig = std::static_pointer_cast(_config); - if (renderContext->jobConfig->alwaysEnabled || renderContext->jobConfig->enabled) { - jobRun(_data, sceneContext, renderContext, _input.get(), _output.edit()); + auto config = std::static_pointer_cast(_config); + if (config->alwaysEnabled || config->enabled) { + for (auto job : _data._jobs) { + job.run(sceneContext, renderContext); + } } - renderContext->jobConfig.reset(); } }; - template using ModelI = Model; - template using ModelO = Model; - template using ModelIO = Model; + template using ModelO = Model; using Jobs = std::vector; @@ -655,6 +654,10 @@ public: return addJob(name, input, std::forward(args)...); } + template void setOutput(O&& output) { + _output = Varying(output); + } + template void createConfiguration() { auto config = std::make_shared(); if (_config) { @@ -688,11 +691,18 @@ public: } } + void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) { + for (auto job : _jobs) { + job.run(sceneContext, renderContext); + } + } + protected: - template friend class Model; + template friend class Model; QConfigPointer _config; Jobs _jobs; + Varying _output; }; } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 03b01e1b9c..6bcf1b1c01 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -908,19 +908,11 @@ void ScriptEngine::run() { 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); + // We only want to sleep a small amount so that any pending events (like timers or invokeMethod events) + // will be able to process quickly. + static const int SMALL_SLEEP_AMOUNT = 100; + auto smallSleepUntil = clock::now() + static_cast(SMALL_SLEEP_AMOUNT); + std::this_thread::sleep_until(smallSleepUntil); } #ifdef SCRIPT_DELAY_DEBUG @@ -1010,21 +1002,6 @@ 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() { @@ -1035,6 +1012,7 @@ void ScriptEngine::stopAllTimers() { stopTimer(timer); } } + void ScriptEngine::stopAllTimersForEntityScript(const EntityItemID& entityID) { // We could maintain a separate map of entityID => QTimer, but someone will have to prove to me that it's worth the complexity. -HRS QVector toDelete; diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 803aa7fa22..d9a1249b1c 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -218,7 +218,6 @@ protected: void init(); bool evaluatePending() const { return _evaluatesPending > 0; } - quint64 getTimersRemainingTime(); void timerFired(); void stopAllTimers(); void stopAllTimersForEntityScript(const EntityItemID& entityID); diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 191952e354..d0ad699846 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -21,9 +21,25 @@ UsersScriptingInterface::UsersScriptingInterface() { connect(nodeList.data(), &NodeList::usernameFromIDReply, this, &UsersScriptingInterface::usernameFromIDReply); } -void UsersScriptingInterface::ignore(const QUuid& nodeID) { +void UsersScriptingInterface::ignore(const QUuid& nodeID, bool ignoreEnabled) { // ask the NodeList to ignore this user (based on the session ID of their node) - DependencyManager::get()->ignoreNodeBySessionID(nodeID); + DependencyManager::get()->ignoreNodeBySessionID(nodeID, ignoreEnabled); +} + +bool UsersScriptingInterface::getIgnoreStatus(const QUuid& nodeID) { + // ask the NodeList for the Ignore status associated with the given session ID + return DependencyManager::get()->isIgnoringNode(nodeID); +} + +void UsersScriptingInterface::personalMute(const QUuid& nodeID, bool muteEnabled) { + // ask the NodeList to mute the user with the given session ID + // "Personal Mute" only applies one way and is not global + DependencyManager::get()->personalMuteNodeBySessionID(nodeID, muteEnabled); +} + +bool UsersScriptingInterface::getPersonalMuteStatus(const QUuid& nodeID) { + // ask the NodeList for the Personal Mute status associated with the given session ID + return DependencyManager::get()->isPersonalMutingNode(nodeID); } void UsersScriptingInterface::kick(const QUuid& nodeID) { @@ -61,3 +77,10 @@ void UsersScriptingInterface::disableIgnoreRadius() { bool UsersScriptingInterface::getIgnoreRadiusEnabled() { return DependencyManager::get()->getIgnoreRadiusEnabled(); } + +bool UsersScriptingInterface::getRequestsDomainListData() { + return DependencyManager::get()->getRequestsDomainListData(); +} +void UsersScriptingInterface::setRequestsDomainListData(bool isRequesting) { + DependencyManager::get()->setRequestsDomainListData(isRequesting); +} \ No newline at end of file diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 855dc06c11..4182d5244c 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -24,6 +24,7 @@ class UsersScriptingInterface : public QObject, public Dependency { SINGLETON_DEPENDENCY Q_PROPERTY(bool canKick READ getCanKick) + Q_PROPERTY(bool requestsDomainListData READ getRequestsDomainListData WRITE setRequestsDomainListData) public: UsersScriptingInterface(); @@ -34,8 +35,31 @@ public slots: * Ignore another user. * @function Users.ignore * @param {nodeID} nodeID The node or session ID of the user you want to ignore. + * @param {bool} enable True for ignored; false for un-ignored. */ - void ignore(const QUuid& nodeID); + void ignore(const QUuid& nodeID, bool ignoreEnabled); + + /**jsdoc + * Gets a bool containing whether you have ignored the given Avatar UUID. + * @function Users.getIgnoreStatus + * @param {nodeID} nodeID The node or session ID of the user whose ignore status you want. + */ + bool getIgnoreStatus(const QUuid& nodeID); + + /**jsdoc + * Mute another user for you and you only. + * @function Users.personalMute + * @param {nodeID} nodeID The node or session ID of the user you want to mute. + * @param {bool} enable True for enabled; false for disabled. + */ + void personalMute(const QUuid& nodeID, bool muteEnabled); + + /**jsdoc + * Requests a bool containing whether you have personally muted the given Avatar UUID. + * @function Users.requestPersonalMuteStatus + * @param {nodeID} nodeID The node or session ID of the user whose personal mute status you want. + */ + bool getPersonalMuteStatus(const QUuid& nodeID); /**jsdoc * Kick another user. @@ -45,7 +69,7 @@ public slots: void kick(const QUuid& nodeID); /**jsdoc - * Mute another user. + * Mute another user for everyone. * @function Users.mute * @param {nodeID} nodeID The node or session ID of the user you want to mute. */ @@ -105,6 +129,11 @@ signals: * @function Users.usernameFromIDReply */ void usernameFromIDReply(const QString& nodeID, const QString& username, const QString& machineFingerprint); + +private: + bool getRequestsDomainListData(); + void setRequestsDomainListData(bool requests); + bool _requestsDomainListData; }; diff --git a/scripts/developer/tests/testInterval.js b/scripts/developer/tests/testInterval.js new file mode 100644 index 0000000000..94a5fe1fa5 --- /dev/null +++ b/scripts/developer/tests/testInterval.js @@ -0,0 +1,244 @@ +// Tester, try testing these different settings. +// +// Changing the TIMER_HZ will show different performance results. It's expected that at 90hz you'll see a fair +// amount of variance, as Qt Timers simply aren't accurate enough. In general RPC peformance should match the timer +// without significant difference in variance. + +var TIMER_HZ = 50; // Change this for different values +var TIMER_INTERVAL = 1000 / TIMER_HZ; +var TIMER_WORK_EFFORT = 0; // 1000 is light work, 1000000 ~= 30ms + +var UPDATE_HZ = 60; // standard script update rate +var UPDATE_INTERVAL = 1000/UPDATE_HZ; // standard script update interval +var UPDATE_WORK_EFFORT = 0; // 1000 is light work, 1000000 ~= 30ms + +var basePosition = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); + +var timerBox = Entities.addEntity( + { type: "Box", + position: basePosition, + dimensions: { x: 0.1, y: 0.1, z: 0.1 }, + color: { red: 255, green: 0, blue: 255 }, + dynamic: false, + collisionless: true + }); + +var lastTick = Date.now(); +var deltaTick = 0; +var tickSamples = 0; +var totalVariance = 0; +var tickCount = 0; +var totalWork = 0; +var highVarianceCount = 0; +var varianceCount = 0; + +print("Set interval = " + TIMER_INTERVAL); + +var timerTime = 0.0; +var range = 0.5; +var rotationFactor = 0.5; // smaller == faster +var omega = 2.0 * Math.PI / rotationFactor; + +var ticker = Script.setInterval(function() { + + tickCount++; + var tickNow = Date.now(); + deltaTick = tickNow - lastTick; + + var variance = Math.abs(deltaTick - TIMER_INTERVAL); + totalVariance += variance; + + if (variance > 1) { + varianceCount++; + } + if (variance > 5) { + highVarianceCount++; + } + + var preWork = Date.now(); + var y = 2; + for (var x = 0; x < TIMER_WORK_EFFORT; x++) { + y = y * y; + } + var postWork = Date.now(); + deltaWork = postWork - preWork; + totalWork += deltaWork; + + // move a box + var deltaTime = deltaTick / 1000; + timerTime += deltaTime; + rotation = Quat.angleAxis(timerTime * omega / Math.PI * 180.0, { x: 0, y: 1, z: 0 }); + Entities.editEntity(timerBox, + { + position: { x: basePosition.x + Math.sin(timerTime * omega) / 2.0 * range, + y: basePosition.y, + z: basePosition.z }, + //rotation: rotation + }); + + tickSamples = tickSamples + deltaTick; + lastTick = tickNow; + + // report about every 5 seconds + if(tickCount == (TIMER_HZ * 5)) { + print("TIMER -- For " + tickCount + " samples average interval = " + tickSamples/tickCount + " ms" + + " average variance:" + totalVariance/tickCount + " ms" + + " min variance:" + varianceCount + " [" + (varianceCount/tickCount) * 100 + " %] " + + " high variance:" + highVarianceCount + " [" + (highVarianceCount/tickCount) * 100 + " %] " + + " average work:" + totalWork/tickCount + " ms"); + tickCount = 0; + tickSamples = 0; + totalWork = 0; + totalVariance = 0; + varianceCount = 0; + highVarianceCount = 0; + } +}, TIMER_INTERVAL); + +///////////////////////////////////////////////////////////////////////// + +var rpcPosition = Vec3.sum(basePosition, { x:0, y: 0.2, z: 0}); + +var theRpcFunctionInclude = Script.resolvePath("testIntervalRpcFunction.js"); + +print("theRpcFunctionInclude:" + theRpcFunctionInclude); + +var rpcBox = Entities.addEntity( + { type: "Box", + position: rpcPosition, + dimensions: { x: 0.1, y: 0.1, z: 0.1 }, + color: { red: 255, green: 255, blue: 0 }, + dynamic: false, + collisionless: true, + script: theRpcFunctionInclude + }); + +var rpcLastTick = Date.now(); +var rpcTotalTicks = 0; +var rpcCount = 0; +var rpcTotalVariance = 0; +var rpcTickCount = 0; +var rpcVarianceCount = 0; +var rpcHighVarianceCount = 0; + +var rpcTicker = Script.setInterval(function() { + + rpcTickCount++; + var tickNow = Date.now(); + var deltaTick = tickNow - rpcLastTick; + var variance = Math.abs(deltaTick - TIMER_INTERVAL); + rpcTotalVariance += variance; + + if (variance > 1) { + rpcVarianceCount++; + } + if (variance > 5) { + rpcHighVarianceCount++; + } + + rpcTotalTicks += deltaTick; + rpcLastTick = tickNow; + + var args = [range, rotationFactor, omega, TIMER_INTERVAL, TIMER_HZ]; + + Entities.callEntityMethod(rpcBox, "doRPC", args); + + // report about every 5 seconds + if(rpcTickCount == (TIMER_HZ * 5)) { + print("RPCTIMER- For " + rpcTickCount + " samples average interval = " + rpcTotalTicks/rpcTickCount + " ms" + + " average variance:" + rpcTotalVariance/rpcTickCount + " ms" + + " min variance:" + rpcVarianceCount + " [" + (rpcVarianceCount/rpcTickCount) * 100 + " %] " + + " high variance:" + rpcHighVarianceCount + " [" + (rpcHighVarianceCount/rpcTickCount) * 100 + " %] " + ); + rpcTickCount = 0; + rpcTotalTicks = 0; + rpcTotalVariance = 0; + rpcVarianceCount = 0; + rpcHighVarianceCount = 0; + } +}, TIMER_INTERVAL); + + +var updateCount = 0; +var updateTotalElapsed = 0; +var lastUpdate = Date.now(); +var updateTotalWork = 0; +var updateTotalVariance = 0; +var updateVarianceCount = 0; +var updateHighVarianceCount = 0; + +var updatePosition = Vec3.sum(basePosition, { x:0, y: -0.2, z: 0}); + +var updateBox = Entities.addEntity( + { type: "Box", + position: updatePosition, + dimensions: { x: 0.1, y: 0.1, z: 0.1 }, + color: { red: 0, green: 255, blue: 255 }, + dynamic: false, + collisionless: true + }); + + +var updateTime = 0; +var updateFunction = function(deltaTime){ + updateCount++; + var updateAt = Date.now(); + deltaUpdate = updateAt - lastUpdate; + updateTotalElapsed += deltaUpdate; + lastUpdate = updateAt; + + var variance = Math.abs(deltaUpdate - UPDATE_INTERVAL); + updateTotalVariance += variance; + + if (variance > 1) { + updateVarianceCount++; + } + + if (variance > 5) { + updateHighVarianceCount++; + } + + var preWork = Date.now(); + var y = 2; + for (var x = 0; x < UPDATE_WORK_EFFORT; x++) { + y = y * y; + } + var postWork = Date.now(); + deltaWork = postWork - preWork; + updateTotalWork += deltaWork; + + // move a box + + updateTime += deltaTime; + rotation = Quat.angleAxis(updateTime * omega / Math.PI * 180.0, { x: 0, y: 1, z: 0 }); + Entities.editEntity(updateBox, + { + position: { x: updatePosition.x + Math.sin(updateTime * omega) / 2.0 * range, + y: updatePosition.y, + z: updatePosition.z }, + }); + + + if(updateCount == (UPDATE_HZ * 5)) { + print("UPDATE -- For " + updateCount + " samples average update = " + updateTotalElapsed/updateCount + " ms" + + " average variance:" + updateTotalVariance/updateCount + " ms" + + " min variance:" + updateVarianceCount + " [" + (updateVarianceCount/updateCount) * 100 + " %] " + + " high variance:" + updateHighVarianceCount + " [" + (updateHighVarianceCount/updateCount) * 100 + " %] " + + " average work:" + updateTotalWork/updateCount + " ms"); + + updateCount = 0; + updateTotalElapsed = 0; + updateTotalWork = 0; + updateTotalVariance = 0; + updateVarianceCount = 0; + updateHighVarianceCount = 0; + } +}; + +Script.update.connect(updateFunction); + +Script.scriptEnding.connect(function(){ + Entities.deleteEntity(timerBox); + Entities.deleteEntity(rpcBox); + Entities.deleteEntity(updateBox); +}); diff --git a/scripts/developer/tests/testIntervalRpcFunction.js b/scripts/developer/tests/testIntervalRpcFunction.js new file mode 100644 index 0000000000..86caef3f61 --- /dev/null +++ b/scripts/developer/tests/testIntervalRpcFunction.js @@ -0,0 +1,77 @@ +(function() { + var x = false; + var y = false; + var z = false; + + var entityLastTick = Date.now(); + var entityTotalTicks = 0; + var entityCount = 0; + var entityTotalVariance = 0; + var entityTickCount = 0; + var entityVarianceCount = 0; + var entityHighVarianceCount = 0; + var _entityID; + var time = 0; + + function Foo() { return; }; + Foo.prototype = { + preload: function(entityID) { print('Foo preload'); _entityID = entityID; }, + doRPC: function(entityID, args) { + var range = args[0]; + var rotationFactor = args[1]; + var omega = args[2]; + var TIMER_INTERVAL = args[3]; + var TIMER_HZ = args[4]; + + // first time, set our x,y,z + if (x === false) { + var position = Entities.getEntityProperties(_entityID, "position").position; + x = position.x; + y = position.y; + z = position.z; + } + entityTickCount++; + var tickNow = Date.now(); + var deltaTick = tickNow - entityLastTick; + var variance = Math.abs(deltaTick - TIMER_INTERVAL); + entityTotalVariance += variance; + + if (variance > 1) { + entityVarianceCount++; + } + if (variance > 5) { + entityHighVarianceCount++; + } + + entityTotalTicks += deltaTick; + entityLastTick = tickNow; + + // move self!! + var deltaTime = deltaTick / 1000; + time += deltaTime; + rotation = Quat.angleAxis(time * omega / Math.PI * 180.0, { x: 0, y: 1, z: 0 }); + Entities.editEntity(_entityID, + { + position: { x: x + Math.sin(time * omega) / 2.0 * range, + y: y, + z: z }, + }); + + + if(entityTickCount == (TIMER_HZ * 5)) { + print("ENTITY -- For " + entityTickCount + " samples average interval = " + entityTotalTicks/entityTickCount + " ms" + + " average variance:" + entityTotalVariance/entityTickCount + " ms" + + " min variance:" + entityVarianceCount + " [" + (entityVarianceCount/entityTickCount) * 100 + " %] " + + " high variance:" + entityHighVarianceCount + " [" + (entityHighVarianceCount/entityTickCount) * 100 + " %] " + ); + entityTickCount = 0; + entityTotalTicks = 0; + entityTotalVariance = 0; + entityVarianceCount = 0; + entityHighVarianceCount = 0; + } + } + }; + + return new Foo(); +}); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 1f536b9567..513e70d18c 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -816,7 +816,6 @@ function MyController(hand) { }; this.update = function(deltaTime, timestamp) { - this.updateSmoothedTrigger(); // If both trigger and grip buttons squeezed and nothing is held, rescale my avatar! @@ -2141,7 +2140,6 @@ function MyController(hand) { }; this.nearGrabbing = function(deltaTime, timestamp) { - this.grabPointSphereOff(); if (this.state == STATE_NEAR_GRABBING && (!this.triggerClicked && this.secondaryReleased())) { @@ -3066,9 +3064,65 @@ var handleHandMessages = function(channel, message, sender) { Messages.messageReceived.connect(handleHandMessages); -var BASIC_TIMER_INTERVAL_MS = 20; // 20ms = 50hz good enough +var TARGET_UPDATE_HZ = 50; // 50hz good enough (no change in logic) +var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; +var lastInterval = Date.now(); + +var intervalCount = 0; +var totalDelta = 0; +var totalVariance = 0; +var highVarianceCount = 0; +var veryhighVarianceCount = 0; +var updateTotalWork = 0; + +var UPDATE_PERFORMANCE_DEBUGGING = false; + var updateIntervalTimer = Script.setInterval(function(){ - update(BASIC_TIMER_INTERVAL_MS / 1000); + + intervalCount++; + var thisInterval = Date.now(); + var deltaTimeMsec = thisInterval - lastInterval; + var deltaTime = deltaTimeMsec / 1000; + lastInterval = thisInterval; + + totalDelta += deltaTimeMsec; + + var variance = Math.abs(deltaTimeMsec - BASIC_TIMER_INTERVAL_MS); + totalVariance += variance; + + if (variance > 1) { + highVarianceCount++; + } + + if (variance > 5) { + veryhighVarianceCount++; + } + + // will call update for both hands + var preWork = Date.now(); + update(deltaTime); + var postWork = Date.now(); + var workDelta = postWork - preWork; + updateTotalWork += workDelta; + + if (intervalCount == 100) { + + if (UPDATE_PERFORMANCE_DEBUGGING) { + print("handControllerGrab.js -- For " + intervalCount + " samples average= " + totalDelta/intervalCount + " ms" + + " average variance:" + totalVariance/intervalCount + " ms" + + " high variance count:" + highVarianceCount + " [ " + (highVarianceCount/intervalCount) * 100 + "% ] " + + " VERY high variance count:" + veryhighVarianceCount + " [ " + (veryhighVarianceCount/intervalCount) * 100 + "% ] " + + " average work:" + updateTotalWork/intervalCount + " ms"); + } + + intervalCount = 0; + totalDelta = 0; + totalVariance = 0; + highVarianceCount = 0; + veryhighVarianceCount = 0; + updateTotalWork = 0; + } + }, BASIC_TIMER_INTERVAL_MS); function cleanup() { diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 9c23e1f775..5eb35c2595 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -132,10 +132,14 @@ function populateUserList() { // Request the username from the given UUID Users.requestUsernameFromID(id); } - data.push(avatarPalDatum); - if (id) { // No overlay for ourself. - addAvatarNode(id); + // Request personal mute status and ignore status + // from NodeList (as long as we're not requesting it for our own ID) + if (id) { + avatarPalDatum['personalMute'] = Users.getPersonalMuteStatus(id); + avatarPalDatum['ignore'] = Users.getIgnoreStatus(id); + addAvatarNode(id); // No overlay for ourselves } + data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); }); pal.sendToQml({method: 'users', params: data}); @@ -252,9 +256,11 @@ function off() { } triggerMapping.disable(); // It's ok if we disable twice. removeOverlays(); + Users.requestsDomainListData = false; } function onClicked() { if (!pal.visible) { + Users.requestsDomainListData = true; populateUserList(); pal.raise(); isWired = true; @@ -326,6 +332,14 @@ button.clicked.connect(onClicked); pal.visibleChanged.connect(onVisibleChanged); pal.closed.connect(off); Users.usernameFromIDReply.connect(usernameFromIDReply); +function clearIgnoredInQMLAndClosePAL() { + pal.sendToQml({ method: 'clearIgnored' }); + if (pal.visible) { + onClicked(); // Close the PAL + } +} +Window.domainChanged.connect(clearIgnoredInQMLAndClosePAL); +Window.domainConnectionRefused.connect(clearIgnoredInQMLAndClosePAL); // // Cleanup. @@ -336,6 +350,8 @@ Script.scriptEnding.connect(function () { pal.visibleChanged.disconnect(onVisibleChanged); pal.closed.disconnect(off); Users.usernameFromIDReply.disconnect(usernameFromIDReply); + Window.domainChanged.disconnect(clearIgnoredInQMLAndClosePAL); + Window.domainConnectionRefused.disconnect(clearIgnoredInQMLAndClosePAL); off(); }); diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index df2c552d1a..7e9d2c426f 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include #include #include @@ -23,7 +25,6 @@ #include #include - #include #include #include @@ -33,7 +34,6 @@ #include #include - #include #include #include @@ -60,8 +60,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -537,7 +539,14 @@ public: _initContext.makeCurrent(); // Render engine init _renderEngine->addJob("RenderShadowTask", _cullFunctor); - _renderEngine->addJob("RenderDeferredTask", _cullFunctor); + const auto items = _renderEngine->addJob("FetchCullSort", _cullFunctor); + assert(items.canCast()); + static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; + if (QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD)) { + _renderEngine->addJob("RenderForwardTask", items.get()); + } else { + _renderEngine->addJob("RenderDeferredTask", items.get()); + } _renderEngine->load(); _renderEngine->registerScene(_main3DScene);