diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..d5c79b8504 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 120 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 60 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs. + Thank you for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 31d8c9e5a8..285c3221f8 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -8,6 +8,7 @@ if (APPLE) endif () setup_memory_debugger() +setup_thread_debugger() # link in the shared libraries link_hifi_libraries( diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index f86dc7f766..c83b95c464 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -269,7 +269,13 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, // the avatar mixer uses the negative value of the sent version instanceVersionRef = -packetTraitVersion; } else { - _avatar->processTraitInstance(traitType, instanceID, message.read(traitSize)); + // Don't accept avatar entity data for distribution unless sender has rez permissions on the domain. + // The sender shouldn't be sending avatar entity data, however this provides a back-up. + auto trait = message.read(traitSize); + if (sendingNode.getCanRezAvatarEntities()) { + _avatar->processTraitInstance(traitType, instanceID, trait); + } + instanceVersionRef = packetTraitVersion; } @@ -290,6 +296,29 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, } } +void AvatarMixerClientData::emulateDeleteEntitiesTraitsMessage(const QList& avatarEntityIDs) { + // Emulates processSetTraitsMessage() actions on behalf of an avatar whose canRezAvatarEntities permission has been removed. + // The source avatar should be removing its avatar entities. However, using this method provides a back-up. + + auto traitType = AvatarTraits::AvatarEntity; + for (const auto& entityID : avatarEntityIDs) { + auto& instanceVersionRef = _lastReceivedTraitVersions.getInstanceValueRef(traitType, entityID); + + _avatar->processDeletedTraitInstance(traitType, entityID); + // Mixer doesn't need deleted IDs. + _avatar->getAndClearRecentlyRemovedIDs(); + + // to track a deleted instance but keep version information + // the avatar mixer uses the negative value of the sent version + // Because there is no originating message from an avatar we enlarge the magnitude by 1. + // If a user subsequently has canRezAvatarEntities permission granted, they will have to relog in order for their + // avatar entities to be visible to others. + instanceVersionRef = -instanceVersionRef - 1; + } + + _lastReceivedTraitsChange = std::chrono::steady_clock::now(); +} + void AvatarMixerClientData::processBulkAvatarTraitsAckMessage(ReceivedMessage& message) { // Avatar Traits flow control marks each outgoing avatar traits packet with a // sequence number. The mixer caches the traits sent in the traits packet. diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 98c8d7e15b..83a2ff384a 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -132,6 +132,7 @@ public: int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode); + void emulateDeleteEntitiesTraitsMessage(const QList& avatarEntityIDs); void processBulkAvatarTraitsAckMessage(ReceivedMessage& message); void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode, AvatarTraits::TraitVersion traitVersion); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 522f0bf163..9a3ef3d0b5 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -432,6 +432,17 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) } } + // The source avatar should be removing its avatar entities. However, provide a back-up. + if (sendAvatar) { + if (!sourceAvatarNode->getCanRezAvatarEntities()) { + auto sourceAvatarNodeData = reinterpret_cast(sourceAvatarNode->getLinkedData()); + auto avatarEntityIDs = sourceAvatarNodeData->getAvatar().getAvatarEntityIDs(); + if (avatarEntityIDs.count() > 0) { + sourceAvatarNodeData->emulateDeleteEntitiesTraitsMessage(avatarEntityIDs); + } + } + } + if (sendAvatar) { AvatarDataSequenceNumber lastSeqToReceiver = destinationNodeData->getLastBroadcastSequenceNumber(sourceAvatarNode->getLocalID()); AvatarDataSequenceNumber lastSeqFromSender = sourceAvatarNodeData->getLastReceivedSequenceNumber(); diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index cbf6ff4eaf..752eaf81d2 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -398,7 +398,7 @@ void ScriptableAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityDa // clear deleted traits for (const auto& id : idsToClear) { - clearAvatarEntity(id); + clearAvatarEntityInternal(id); } } @@ -408,7 +408,7 @@ void ScriptableAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArra std::map::iterator itr = _entities.find(entityID); if (itr != _entities.end()) { _entities.erase(itr); - clearAvatarEntity(entityID); + clearAvatarEntityInternal(entityID); } return; } diff --git a/cmake/macros/LinkHifiLibraries.cmake b/cmake/macros/LinkHifiLibraries.cmake index 6a430f5b13..390bdf2326 100644 --- a/cmake/macros/LinkHifiLibraries.cmake +++ b/cmake/macros/LinkHifiLibraries.cmake @@ -26,5 +26,6 @@ function(LINK_HIFI_LIBRARIES) endforeach() setup_memory_debugger() + setup_thread_debugger() endfunction() diff --git a/cmake/macros/MemoryDebugger.cmake b/cmake/macros/MemoryDebugger.cmake index 09716715f0..2fedc4e7dc 100644 --- a/cmake/macros/MemoryDebugger.cmake +++ b/cmake/macros/MemoryDebugger.cmake @@ -8,23 +8,34 @@ # macro(SETUP_MEMORY_DEBUGGER) -if (DEFINED ENV{HIFI_MEMORY_DEBUGGING}) - SET( HIFI_MEMORY_DEBUGGING true ) +if ("$ENV{VIRCADIA_MEMORY_DEBUGGING}") + if (VIRCADIA_THREAD_DEBUGGING) + message(FATAL_ERROR "Thread debugging and memory debugging can't be enabled at the same time." ) + endif() + + SET( VIRCADIA_MEMORY_DEBUGGING true ) endif () -if (HIFI_MEMORY_DEBUGGING) +if (VIRCADIA_MEMORY_DEBUGGING) if (UNIX) if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") # for clang on Linux - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer -shared-libasan -fsanitize=undefined -fsanitize=address -fsanitize-recover=address") - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -shared-libasan -fsanitize=undefined -fsanitize=address -fsanitize-recover=address") - SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -shared-libasan -fsanitize=undefined -fsanitize=address -fsanitize-recover=address") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-omit-frame-pointer -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address") else () # for gcc on Linux - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined -fsanitize=address -U_FORTIFY_SOURCE -fno-stack-protector -fno-omit-frame-pointer") - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=undefined -fsanitize=address") - SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=undefined -fsanitize=address") + # For some reason, using -fstack-protector results in this error: + # usr/bin/ld: ../../libraries/audio/libaudio.so: undefined reference to `FIR_1x4_AVX512(float*, float*, float*, float*, float*, float (*) [64], int)' + # The '-DSTACK_PROTECTOR' argument below disables the usage of this function in the code. This should be fine as it only works on the latest Intel hardware, + # and is an optimization that should make no functional difference. + + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -U_FORTIFY_SOURCE -DSTACK_PROTECTOR -fstack-protector-strong -fno-omit-frame-pointer") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak ") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak") endif() - endif (UNIX) + else() + message(FATAL_ERROR "Memory debugging is not supported on this platform." ) + endif() endif () endmacro(SETUP_MEMORY_DEBUGGER) diff --git a/cmake/macros/SetupHifiLibrary.cmake b/cmake/macros/SetupHifiLibrary.cmake index 108786a651..c99ba9734e 100644 --- a/cmake/macros/SetupHifiLibrary.cmake +++ b/cmake/macros/SetupHifiLibrary.cmake @@ -53,6 +53,7 @@ macro(SETUP_HIFI_LIBRARY) endforeach() setup_memory_debugger() + setup_thread_debugger() # create a library and set the property so it can be referenced later if (${${TARGET_NAME}_SHARED}) diff --git a/cmake/macros/ThreadDebugger.cmake b/cmake/macros/ThreadDebugger.cmake new file mode 100644 index 0000000000..0353e3b1cb --- /dev/null +++ b/cmake/macros/ThreadDebugger.cmake @@ -0,0 +1,36 @@ +# +# MemoryDebugger.cmake +# +# Copyright 2021 Vircadia Contributors +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(SETUP_THREAD_DEBUGGER) +if ("$ENV{VIRCADIA_THREAD_DEBUGGING}") + if (VIRCADIA_MEMORY_DEBUGGING ) + message(FATAL_ERROR "Thread debugging and memory debugging can't be enabled at the same time." ) + endif () + + SET(VIRCADIA_THREAD_DEBUGGING true) +endif () + +if (VIRCADIA_THREAD_DEBUGGING) + if (UNIX) + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # for clang on Linux + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread -fno-omit-frame-pointer") + SET(CMAKE_EXE_LINKER_FLAGS "-fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}") + SET(CMAKE_SHARED_LINKER_FLAGS "-fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}") + else () + # for gcc on Linux + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread -fno-omit-frame-pointer") + SET(CMAKE_EXE_LINKER_FLAGS " -fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}") + SET(CMAKE_SHARED_LINKER_FLAGS "-fsanitize=thread ${CMAKE_EXE_LINKER_FLAGS}") + endif() + else() + message(FATAL_ERROR "Thread debugging is not supported on this platform.") + endif() +endif () +endmacro(SETUP_THREAD_DEBUGGER) diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index 693132a8f7..a3a85684b4 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -15,6 +15,7 @@ if (APPLE) endif () setup_memory_debugger() +setup_thread_debugger() # TODO: find a solution that will handle web file changes in resources on windows without a re-build. # Currently the resources are only copied on post-build. If one is changed but the domain-server is not, they will diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 32868c9b80..b91a10a17c 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.4, + "version": 2.5, "settings": [ { "name": "metaverse", @@ -295,10 +295,10 @@ }, { "name": "approved_safe_urls", - "label": "Approved Script and QML URLs", + "label": "Approved Script and QML URLs (Not Enabled)", "help": "These URLs will be sent to the Interface as safe URLs to allow through the whitelist if the Interface has this security option enabled.", - "placeholder": "0", - "default": "1", + "placeholder": "", + "default": "", "advanced": false }, { @@ -338,7 +338,7 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which types of users can have which domain-wide permissions.", + "help": "Indicate which types of users can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, "groups": [ @@ -347,8 +347,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -363,6 +363,13 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_avatar_entities", + "label": "Avatar Entities", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_adjust_locks", "label": "Lock / Unlock", @@ -439,17 +446,20 @@ "default": [ { "id_can_connect": true, + "id_can_rez_avatar_entities": true, "id_can_rez_tmp_certified": true, "permissions_id": "anonymous" }, { "id_can_connect": true, + "id_can_rez_avatar_entities": true, "id_can_rez_tmp_certified": true, "permissions_id": "friends" }, { - "id_can_adjust_locks": true, "id_can_connect": true, + "id_can_rez_avatar_entities": true, + "id_can_adjust_locks": true, "id_can_connect_past_max_capacity": true, "id_can_kick": true, "id_can_replace_content": true, @@ -463,6 +473,7 @@ }, { "id_can_connect": true, + "id_can_rez_avatar_entities": true, "id_can_rez_tmp_certified": true, "permissions_id": "logged-in" } @@ -484,8 +495,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -525,6 +536,13 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_avatar_entities", + "label": "Avatar Entities", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_adjust_locks", "label": "Lock / Unlock", @@ -613,8 +631,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -651,6 +669,13 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_avatar_entities", + "label": "Avatar Entities", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_adjust_locks", "label": "Lock / Unlock", @@ -734,8 +759,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -750,6 +775,13 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_avatar_entities", + "label": "Avatar Entities", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_adjust_locks", "label": "Lock / Unlock", @@ -833,8 +865,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -849,6 +881,13 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_avatar_entities", + "label": "Avatar Entities", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_adjust_locks", "label": "Lock / Unlock", @@ -932,8 +971,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -948,6 +987,13 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_avatar_entities", + "label": "Avatar Entities", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_adjust_locks", "label": "Lock / Unlock", @@ -1031,8 +1077,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -1047,6 +1093,13 @@ "editable": true, "default": false }, + { + "name": "id_can_rez_avatar_entities", + "label": "Avatar Entities", + "type": "checkbox", + "editable": true, + "default": false + }, { "name": "id_can_adjust_locks", "label": "Lock / Unlock", diff --git a/domain-server/resources/web/wizard/js/wizard.js b/domain-server/resources/web/wizard/js/wizard.js index 38842cf79f..2ce23d3fcf 100644 --- a/domain-server/resources/web/wizard/js/wizard.js +++ b/domain-server/resources/web/wizard/js/wizard.js @@ -465,6 +465,7 @@ function savePermissions() { "standard_permissions": [ { "id_can_connect": anonymousCanConnect, + "id_can_rez_avatar_entities": anonymousCanConnect, "id_can_rez": anonymousCanRez, "id_can_rez_certified": anonymousCanRez, "id_can_rez_tmp": anonymousCanRez, @@ -473,6 +474,7 @@ function savePermissions() { }, { "id_can_connect": friendsCanConnect, + "id_can_rez_avatar_entities": friendsCanConnect, "id_can_rez": friendsCanRez, "id_can_rez_certified": friendsCanRez, "id_can_rez_tmp": friendsCanRez, @@ -481,6 +483,7 @@ function savePermissions() { }, { "id_can_connect": loggedInCanConnect, + "id_can_rez_avatar_entities": loggedInCanConnect, "id_can_rez": loggedInCanRez, "id_can_rez_certified": loggedInCanRez, "id_can_rez_tmp": loggedInCanRez, @@ -490,6 +493,7 @@ function savePermissions() { { "id_can_adjust_locks": localhostPermissions, "id_can_connect": localhostPermissions, + "id_can_rez_avatar_entities": localhostPermissions, "id_can_connect_past_max_capacity": localhostPermissions, "id_can_kick": localhostPermissions, "id_can_replace_content": localhostPermissions, diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 7126c525bc..035caaa328 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -353,6 +353,7 @@ void DomainGatekeeper::updateNodePermissions() { userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; + userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities; } else { // at this point we don't have a sending socket for packets from this node - assume it is the active socket // or the public socket if we haven't activated a socket for the node yet @@ -448,6 +449,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; + userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities; newNode->setPermissions(userPerms); return newNode; } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index bb79bb367a..f0a648b7c0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -66,6 +66,7 @@ using namespace std::chrono; Q_LOGGING_CATEGORY(domain_server, "hifi.domain_server") Q_LOGGING_CATEGORY(domain_server_ice, "hifi.domain_server.ice") +Q_LOGGING_CATEGORY(domain_server_auth, "vircadia.domain_server.auth") const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace"; @@ -1545,9 +1546,9 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress, const static const QString PORT_SETTINGS_KEY = "domain_server." + PUBLIC_SOCKET_PORT_KEY; const int portFromSettings = _settingsManager.valueForKeyPath(PORT_SETTINGS_KEY).toInt(); - if (port != NULL) { + if (port != 0) { domainObject[PUBLIC_SOCKET_PORT_KEY] = port; - } else if (portFromSettings != NULL) { + } else if (portFromSettings != 0) { domainObject[PUBLIC_SOCKET_PORT_KEY] = portFromSettings; } @@ -2771,6 +2772,20 @@ void DomainServer::profileRequestFinished() { } } +QString DomainServer::operationToString(const QNetworkAccessManager::Operation &op) { + switch(op) { + case QNetworkAccessManager::Operation::HeadOperation: return "HEAD"; + case QNetworkAccessManager::Operation::GetOperation: return "GET"; + case QNetworkAccessManager::Operation::PutOperation: return "PUT"; + case QNetworkAccessManager::Operation::PostOperation: return "POST"; + case QNetworkAccessManager::Operation::DeleteOperation: return "DELETE"; + case QNetworkAccessManager::Operation::CustomOperation: return "CUSTOM"; + case QNetworkAccessManager::Operation::UnknownOperation: + default: + return "UNKNOWN"; + } +} + std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* connection) { static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie"; @@ -2784,6 +2799,9 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY); QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY); + QString httpPeerAddress = connection->peerAddress().toString(); + QString httpOperation = operationToString(connection->requestOperation()); + if (_oauthEnable) { QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY); @@ -2817,11 +2835,15 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c foreach(const QString& userRole, sessionData.getRoles()) { if (adminRolesArray.contains(userRole)) { // this user has a role that allows them to administer the domain-server + qCInfo(domain_server_auth) << httpPeerAddress << "- OAuth:" << profileUsername << " - " + << httpOperation << " " << connection->requestUrl(); return { true, profileUsername }; } } } + qCWarning(domain_server_auth) << httpPeerAddress << "- OAuth authentication failed for " << profileUsername << "-" + << httpOperation << " " << connection->requestUrl(); connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY); // the user does not have allowed username or role, return 401 @@ -2833,6 +2855,9 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c if (connection->requestHeader(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) { // unauthorized XHR requests get a 401 and not a 302, since there isn't an XHR // path to OAuth authorize + + qCWarning(domain_server_auth) << httpPeerAddress << "- OAuth unauthorized XHR -" + << httpOperation << " " << connection->requestUrl(); connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY); } else { // re-direct this user to OAuth page @@ -2849,6 +2874,8 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c redirectHeaders.insert("Location", authURL.toEncoded()); + qCWarning(domain_server_auth) << httpPeerAddress << "- OAuth redirecting -" + << httpOperation << " " << connection->requestUrl(); connection->respond(HTTPConnection::StatusCode302, QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders); } @@ -2883,7 +2910,12 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c "" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex(); if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) { + qCInfo(domain_server_auth) << httpPeerAddress << "- Basic:" << headerUsername << "-" + << httpOperation << " " << connection->requestUrl(); return { true, headerUsername }; + } else { + qCWarning(domain_server_auth) << httpPeerAddress << "- Basic auth failed for" << headerUsername << "-" + << httpOperation << " " << connection->requestUrl(); } } } @@ -2904,11 +2936,13 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY, HTTPConnection::DefaultContentType, basicAuthHeader); + qCWarning(domain_server_auth) << httpPeerAddress << "- Basic auth required -" << httpOperation << " " << connection->requestUrl(); // not authenticated, bubble up false return { false, QString() }; } else { // we don't have an OAuth URL + admin roles/usernames, so all users are authenticated + qCWarning(domain_server_auth) << httpPeerAddress << "- OPEN ACCESS -" << httpOperation << " " << connection->requestUrl(); return { true, QString() }; } } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 24d26540c1..11d04cb255 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -43,6 +43,7 @@ Q_DECLARE_LOGGING_CATEGORY(domain_server) Q_DECLARE_LOGGING_CATEGORY(domain_server_ice) +Q_DECLARE_LOGGING_CATEGORY(domain_server_auth) typedef QSharedPointer SharedAssignmentPointer; typedef QMultiHash TransactionHash; @@ -233,6 +234,8 @@ private: std::initializer_list optionalData = { }, bool requireAccessToken = true); + QString operationToString(const QNetworkAccessManager::Operation &op); + SubnetList _acSubnetWhitelist; std::vector _replicatedUsernames; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 08cf56b188..7b04c72845 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include "DomainServerNodeData.h" @@ -527,6 +528,28 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena *newAdminRoles = adminRoles; } + if (oldVersion < 2.5) { + // Default values for new canRezAvatarEntities permission. + unpackPermissions(); + std::list> permissionsSets{ + _standardAgentPermissions.get(), + _agentPermissions.get(), + _ipPermissions.get(), + _macPermissions.get(), + _machineFingerprintPermissions.get(), + _groupPermissions.get(), + _groupForbiddens.get() + }; + foreach (auto permissionsSet, permissionsSets) { + for (auto entry : permissionsSet) { + const auto& userKey = entry.first; + if (permissionsSet[userKey]->can(NodePermissions::Permission::canConnectToDomain)) { + permissionsSet[userKey]->set(NodePermissions::Permission::canRezAvatarEntities); + } + } + } + packPermissions(); + } // write the current description version to our settings *versionVariant = _descriptionVersion; @@ -863,6 +886,20 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointerreadWithoutCopy(NUM_BYTES_RFC4122_UUID)); + bool hasOptionalBanParameters = false; + int banParameters; + bool banByUsername; + bool banByFingerprint; + bool banByIP; + // pull optional ban parameters from the packet + if (message.data()->getSize() == (NUM_BYTES_RFC4122_UUID + sizeof(int))) { + hasOptionalBanParameters = true; + message->readPrimitive(&banParameters); + banByUsername = banParameters & ModerationFlags::BanFlags::BAN_BY_USERNAME; + banByFingerprint = banParameters & ModerationFlags::BanFlags::BAN_BY_FINGERPRINT; + banByIP = banParameters & ModerationFlags::BanFlags::BAN_BY_IP; + } + if (!nodeUUID.isNull() && nodeUUID != sendingNode->getUUID()) { // make sure we actually have a node with this UUID auto limitedNodeList = DependencyManager::get(); @@ -881,16 +918,20 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetPermissions().getKey()]; + // grab or create permissions for the given username + auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; - newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain); + newPermissions = + !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain); - // ensure that the connect permission is clear - userPermissions->clear(NodePermissions::Permission::canConnectToDomain); + // ensure that the connect permission is clear + userPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } } // if we didn't have a username, or this domain-server uses the "multi-kick" setting to @@ -898,7 +939,7 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer(matchingNode->getLinkedData()); if (nodeData) { @@ -923,36 +964,39 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointerclear(NodePermissions::Permission::canConnectToDomain); } } else { - // if no node data, all we can do is IP address - auto& kickAddress = matchingNode->getActiveSocket() - ? matchingNode->getActiveSocket()->getAddress() - : matchingNode->getPublicSocket().getAddress(); + // if no node data, all we can do is ban by IP address + banByIP = true; + } + } + + if (banByIP) { + auto& kickAddress = matchingNode->getActiveSocket() + ? matchingNode->getActiveSocket()->getAddress() + : matchingNode->getPublicSocket().getAddress(); - // probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server - // it is always 127.0.0.1, but looking at the public and local addresses just to be sure - // TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we - // do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it. - if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() || - kickAddress == limitedNodeList->getLocalSockAddr().getAddress() || - kickAddress.isLoopback() ) { - qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest"; - return; - } + // probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server + // it is always 127.0.0.1, but looking at the public and local addresses just to be sure + // TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we + // do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it. + if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() || + kickAddress == limitedNodeList->getLocalSockAddr().getAddress() || + kickAddress.isLoopback() ) { + qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest"; + return; + } + NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid()); - NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid()); + // check if there were already permissions for the IP + bool hadIPPermissions = hasPermissionsForIP(kickAddress); - // check if there were already permissions for the IP - bool hadIPPermissions = hasPermissionsForIP(kickAddress); + // grab or create permissions for the given IP address + auto ipPermissions = _ipPermissions[ipAddressKey]; - // grab or create permissions for the given IP address - auto ipPermissions = _ipPermissions[ipAddressKey]; + if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; - if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) { - newPermissions = true; - - ipPermissions->clear(NodePermissions::Permission::canConnectToDomain); - } + ipPermissions->clear(NodePermissions::Permission::canConnectToDomain); } } @@ -1448,6 +1492,8 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt SettingsBackupFlag settingsBackupFlag) { QJsonObject responseObject; + responseObject["version"] = _descriptionVersion; // Domain settings version number. + if (!typeValue.isEmpty() || authentication == Authenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); diff --git a/ice-server/CMakeLists.txt b/ice-server/CMakeLists.txt index 07b90b369e..9234d68faf 100644 --- a/ice-server/CMakeLists.txt +++ b/ice-server/CMakeLists.txt @@ -19,6 +19,7 @@ endif () include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") setup_memory_debugger() +setup_thread_debugger() # append OpenSSL to our list of libraries to link target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES}) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 0a0ade149d..6e4837527e 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -47,6 +47,7 @@ add_custom_target(resources ALL DEPENDS ${GENERATE_QRC_DEPENDS}) set(OPTIONAL_EXTERNALS "LeapMotion") setup_memory_debugger() +setup_thread_debugger() foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) string(TOUPPER ${EXTERNAL} ${EXTERNAL}_UPPERCASE) @@ -299,7 +300,7 @@ target_link_libraries( ${PLATFORM_QT_LIBRARIES} ) -if (UNIX AND NOT ANDROID) +if (UNIX AND NOT ANDROID AND NOT VIRCADIA_THREAD_DEBUGGING) if (CMAKE_SYSTEM_NAME MATCHES "Linux") # Linux target_link_libraries(${TARGET_NAME} pthread atomic) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index cfc1121af9..2c8d62a598 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -518,7 +518,12 @@ Rectangle { glyphText: "\ue02e" onClicked: { - adjustWearables.open(currentAvatar); + if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) { + adjustWearables.open(currentAvatar); + } else { + Window.alert("You cannot use wearables on this domain.") + } + } } @@ -529,7 +534,11 @@ Rectangle { glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock; onClicked: { - emitSendToScript({'method' : 'toggleWearablesFrozen'}); + if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) { + emitSendToScript({'method' : 'toggleWearablesFrozen'}); + } else { + Window.alert("You cannot use wearables on this domain.") + } } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 55f2bb80b1..15fa8df93d 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -478,7 +478,7 @@ Rectangle { visible: iAmAdmin; role: "mute"; title: "SILENCE"; - width: actionButtonWidth; + width: actionButtonWidth - 8; movable: false; resizable: false; } @@ -506,6 +506,7 @@ Rectangle { id: itemCell; property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"; property bool isButton: styleData.role === "mute" || styleData.role === "kick"; + property bool isBan: styleData.role === "kick"; property bool isAvgAudio: styleData.role === "avgAudioLevel"; opacity: !isButton ? (model && model.isPresent ? 1.0 : 0.4) : 1.0; // Admin actions shouldn't turn gray @@ -605,7 +606,9 @@ Rectangle { color: 2; // Red visible: isButton; enabled: !nameCard.isReplicated; - anchors.centerIn: parent; + anchors.verticalCenter: itemCell.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: styleData.role === "kick" ? 1 : 14; width: 32; height: 32; onClicked: { @@ -620,7 +623,39 @@ Rectangle { HiFiGlyphs { text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted; // Size - size: parent.height*1.3; + 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]; + } + } + + HifiControlsUit.Button { + id: hardBanButton; + color: 2; // Red + visible: isBan; + enabled: !nameCard.isReplicated; + anchors.verticalCenter: itemCell.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: actionButton.width + 3; + width: 32; + height: 32; + onClicked: { + Users[styleData.role](model.sessionId, Users.BAN_BY_USERNAME | Users.BAN_BY_FINGERPRINT | Users.BAN_BY_IP); + UserActivityLogger["palAction"](styleData.role, model.sessionId); + if (styleData.role === "kick") { + nearbyUserModelData.splice(model.userIndex, 1); + nearbyUserModel.remove(model.userIndex); // after changing nearbyUserModelData, b/c ListModel can frob the data + } + } + // muted/error glyphs + HiFiGlyphs { + text: hifi.glyphs.alert; + // Size + size: parent.height * 1.3; // Anchors anchors.fill: parent; // Style @@ -720,7 +755,8 @@ Rectangle { onClicked: letterbox(hifi.glyphs.question, "Admin Actions", "Silence mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.

" + - "Ban removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page."); + "Ban (left) identifies a user by username (if applicable) and machine fingerprint, then removes them from this domain and prevents them from returning. Admins can un-ban users from the Server Domain Settings page.

" + + "Hard Ban (right) identifies a user by username (if applicable), machine fingerprint, and IP address, then removes them from this domain and prevents them from returning. Admins can un-ban users from the Server Domain Settings page."); onEntered: adminHelpText.color = "#94132e"; onExited: adminHelpText.color = hifi.colors.redHighlight; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d8338162b6..057f32f370 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1306,6 +1306,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityServerConnectionTimer.setSingleShot(true); connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); + connect(&domainHandler, &DomainHandler::confirmConnectWithoutAvatarEntities, + this, &Application::confirmConnectWithoutAvatarEntities); + connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { if (!isServerlessMode()) { _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); @@ -2493,7 +2496,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return viewFrustum.getPosition(); }); - DependencyManager::get()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); }); + DependencyManager::get()->setKickConfirmationOperator([this] (const QUuid& nodeID, unsigned int banFlags) { userKickConfirmation(nodeID, banFlags); }); render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([=](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { bool isTablet = url == TabletScriptingInterface::QML; @@ -3575,7 +3578,7 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { _desktopRootItemCreated = true; } -void Application::userKickConfirmation(const QUuid& nodeID) { +void Application::userKickConfirmation(const QUuid& nodeID, unsigned int banFlags) { auto avatarHashMap = DependencyManager::get(); auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); @@ -3600,7 +3603,7 @@ void Application::userKickConfirmation(const QUuid& nodeID) { // ask the NodeList to kick the user with the given session ID if (yes) { - DependencyManager::get()->kickNodeBySessionID(nodeID); + DependencyManager::get()->kickNodeBySessionID(nodeID, banFlags); } DependencyManager::get()->setWaitForKickResponse(false); @@ -9174,6 +9177,32 @@ void Application::setShowBulletConstraintLimits(bool value) { _physicsEngine->setShowBulletConstraintLimits(value); } +void Application::confirmConnectWithoutAvatarEntities() { + + if (_confirmConnectWithoutAvatarEntitiesDialog) { + // Dialog is already displayed. + return; + } + + if (!getMyAvatar()->hasAvatarEntities()) { + // No avatar entities so continue with login. + DependencyManager::get()->getDomainHandler().setCanConnectWithoutAvatarEntities(true); + return; + } + + QString continueMessage = "Your wearables will not display on this domain. Continue?"; + _confirmConnectWithoutAvatarEntitiesDialog = OffscreenUi::asyncQuestion("Continue Without Wearables", continueMessage, + QMessageBox::Yes | QMessageBox::No); + if (_confirmConnectWithoutAvatarEntitiesDialog->getDialogItem()) { + QObject::connect(_confirmConnectWithoutAvatarEntitiesDialog, &ModalDialogListener::response, this, [=](QVariant answer) { + QObject::disconnect(_confirmConnectWithoutAvatarEntitiesDialog, &ModalDialogListener::response, this, nullptr); + _confirmConnectWithoutAvatarEntitiesDialog = nullptr; + bool shouldConnect = (static_cast(answer.toInt()) == QMessageBox::Yes); + DependencyManager::get()->getDomainHandler().setCanConnectWithoutAvatarEntities(shouldConnect); + }); + } +} + void Application::createLoginDialog() { const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; const auto OFFSET = glm::vec2(0.7f, -0.1f); diff --git a/interface/src/Application.h b/interface/src/Application.h index 5cb5fdd5c0..e771d7ea38 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -50,6 +50,8 @@ #include #include #include +#include +#include #include "avatar/MyAvatar.h" #include "FancyCamera.h" @@ -325,6 +327,8 @@ public: int getOtherAvatarsReplicaCount() { return DependencyManager::get()->getReplicaCount(); } void setOtherAvatarsReplicaCount(int count) { DependencyManager::get()->setReplicaCount(count); } + void confirmConnectWithoutAvatarEntities(); + bool getLoginDialogPoppedUp() const { return _loginDialogPoppedUp; } void createLoginDialog(); void updateLoginDialogPosition(); @@ -608,7 +612,7 @@ private: void toggleTabletUI(bool shouldOpen = false) const; bool shouldCaptureMouse() const; - void userKickConfirmation(const QUuid& nodeID); + void userKickConfirmation(const QUuid& nodeID, unsigned int banFlags = ModerationFlags::getDefaultBanFlags()); MainWindow* _window; QElapsedTimer& _sessionRunTimer; @@ -723,6 +727,8 @@ private: bool _loginDialogPoppedUp{ false }; bool _desktopRootItemCreated{ false }; + ModalDialogListener* _confirmConnectWithoutAvatarEntitiesDialog { nullptr }; + bool _developerMenuVisible{ false }; QString _previousAvatarSkeletonModel; float _previousAvatarTargetScale; diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index 070015f05b..cd5235e42b 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -35,6 +35,7 @@ void ConnectionMonitor::init() { connect(&domainHandler, &DomainHandler::connectedToDomain, this, &ConnectionMonitor::stopTimer); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer); connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &ConnectionMonitor::stopTimer); + connect(&domainHandler, &DomainHandler::confirmConnectWithoutAvatarEntities, this, &ConnectionMonitor::stopTimer); connect(this, &ConnectionMonitor::setRedirectErrorState, &domainHandler, &DomainHandler::setRedirectErrorState); auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::loginComplete, this, &ConnectionMonitor::startTimer); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 471645e342..3a320cc628 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -556,8 +556,6 @@ Menu::Menu() { }); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false); - addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ToggleHipsFollowing, 0, false, - avatar.get(), SLOT(setToggleHips(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawBaseOfSupport, 0, false, avatar.get(), SLOT(setEnableDebugDrawBaseOfSupport(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawDefaultPose, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index cac8e77f9e..5cd4c2112e 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -211,7 +211,6 @@ namespace MenuOption { const QString ThirdPerson = "Third Person Legacy"; const QString ThreePointCalibration = "3 Point Calibration"; const QString ThrottleFPSIfNotFocus = "Throttle FPS If Not Focus"; // FIXME - this value duplicated in Basic2DWindowOpenGLDisplayPlugin.cpp - const QString ToggleHipsFollowing = "Toggle Hips Following"; const QString ToolWindow = "Tool Window"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 91d339a38d..e4b7176a01 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -544,7 +544,7 @@ void AvatarManager::removeDeadAvatarEntities(const SetOfEntities& deadEntities) QUuid entityOwnerID = entity->getOwningAvatarID(); AvatarSharedPointer avatar = getAvatarBySessionID(entityOwnerID); if (avatar) { - avatar->clearAvatarEntity(entity->getID()); + avatar->clearAvatarEntityInternal(entity->getID()); } } } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0f66f3bb41..e8c91e7973 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -278,6 +278,9 @@ MyAvatar::MyAvatar(QThread* thread) : // when we leave a domain we lift whatever restrictions that domain may have placed on our scale connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::leaveDomain); + auto nodeList = DependencyManager::get(); + connect(nodeList.data(), &NodeList::canRezAvatarEntitiesChanged, this, &MyAvatar::handleCanRezAvatarEntitiesChanged); + _bodySensorMatrix = deriveBodyFromHMDSensor(); using namespace recording; @@ -365,12 +368,20 @@ MyAvatar::MyAvatar(QThread* thread) : connect(&(_skeletonModel->getRig()), &Rig::onLoadFailed, this, &MyAvatar::onLoadFailed); _characterController.setDensity(_density); + + _addAvatarEntitiesToTreeTimer.setSingleShot(true); + connect(&_addAvatarEntitiesToTreeTimer, &QTimer::timeout, [this] { + addAvatarEntitiesToTree(); + }); } MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); delete _scriptEngine; _scriptEngine = nullptr; + if (_addAvatarEntitiesToTreeTimer.isActive()) { + _addAvatarEntitiesToTreeTimer.stop(); + } } QString MyAvatar::getDominantHand() const { @@ -1393,15 +1404,9 @@ float loadSetting(Settings& settings, const QString& name, float defaultValue) { } void MyAvatar::setToggleHips(bool followHead) { - _follow.setToggleHipsFollowing(followHead); -} - -void MyAvatar::FollowHelper::setToggleHipsFollowing(bool followHead) { - _toggleHipsFollowing = followHead; -} - -bool MyAvatar::FollowHelper::getToggleHipsFollowing() const { - return _toggleHipsFollowing; + Q_UNUSED(followHead); + qCDebug(interfaceapp) << "MyAvatar.setToggleHips is deprecated; it no longer does anything; it will soon be removed from the API; " + "please update your script"; } void MyAvatar::setEnableDebugDrawBaseOfSupport(bool isEnabled) { @@ -1533,7 +1538,23 @@ void MyAvatar::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteAr void MyAvatar::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) { // NOTE: the requiresRemovalFromTree argument is unused - AvatarData::clearAvatarEntity(entityID); + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring clearAvatarEntity() because don't have canRezAvatarEntities permission on domain"; + return; + } + + clearAvatarEntityInternal(entityID); +} + +void MyAvatar::clearAvatarEntityInternal(const QUuid& entityID) { + AvatarData::clearAvatarEntityInternal(entityID); + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + // Don't delete potentially non-rezzed avatar entities from cache, otherwise they're removed from settings. + return; + } + _avatarEntitiesLock.withWriteLock([&] { _cachedAvatarEntityBlobsToDelete.push_back(entityID); }); @@ -1564,6 +1585,39 @@ void MyAvatar::sanitizeAvatarEntityProperties(EntityItemProperties& properties) properties.markAllChanged(); } +void MyAvatar::addAvatarEntitiesToTree() { + AvatarEntityMap::const_iterator constItr = _cachedAvatarEntityBlobs.begin(); + while (constItr != _cachedAvatarEntityBlobs.end()) { + QUuid id = constItr.key(); + _entitiesToAdd.push_back(id); // worked once: hat shown. then unshown when permissions removed but then entity was deleted somewhere along the line! + ++constItr; + } +} + +bool MyAvatar::hasAvatarEntities() const { + return _cachedAvatarEntityBlobs.count() > 0; +} + +void MyAvatar::handleCanRezAvatarEntitiesChanged(bool canRezAvatarEntities) { + if (canRezAvatarEntities) { + // Start displaying avatar entities. + // Allow time for the avatar mixer to be updated with the user's permissions so that it doesn't discard the avatar + // entities sent. In theory, typical worst case would be Interface running on same PC as server and the timings of + // Interface and the avatar mixer sending DomainListRequest to the domain server being such that the avatar sends its + // DomainListRequest and gets its DomainList response DOMAIN_SERVER_CHECK_IN_MSECS after Interface does. Allow extra + // time in case the avatar mixer is bogged down. + _addAvatarEntitiesToTreeTimer.start(5 * DOMAIN_SERVER_CHECK_IN_MSECS); // Single-shot. + } else { + // Cancel any pending addAvatarEntitiesToTree() call. + if (_addAvatarEntitiesToTreeTimer.isActive()) { + _addAvatarEntitiesToTreeTimer.stop(); + } + + // Stop displaying avatar entities. + removeAvatarEntitiesFromTree(); + } +} + void MyAvatar::handleChangedAvatarEntityData() { // NOTE: this is a per-frame update if (getID().isNull() || @@ -1583,6 +1637,8 @@ void MyAvatar::handleChangedAvatarEntityData() { return; } + bool canRezAvatarEntites = DependencyManager::get()->getThisNodeCanRezAvatarEntities(); + // We collect changes to AvatarEntities and then handle them all in one spot per frame: handleChangedAvatarEntityData(). // Basically this is a "transaction pattern" with an extra complication: these changes can come from two // "directions" and the "authoritative source" of each direction is different, so we maintain two distinct sets @@ -1669,12 +1725,15 @@ void MyAvatar::handleChangedAvatarEntityData() { continue; } sanitizeAvatarEntityProperties(properties); - entityTree->withWriteLock([&] { - EntityItemPointer entity = entityTree->addEntity(id, properties); - if (entity) { - packetSender->queueEditAvatarEntityMessage(entityTree, id); - } - }); + if (canRezAvatarEntites) { + entityTree->withWriteLock([&] { + EntityItemPointer entity = entityTree->addEntity(id, properties); + if (entity) { + packetSender->queueEditAvatarEntityMessage(entityTree, id); + } + }); + } + } // CHANGE real entities @@ -1692,7 +1751,7 @@ void MyAvatar::handleChangedAvatarEntityData() { skip = true; } }); - if (!skip) { + if (!skip && canRezAvatarEntites) { sanitizeAvatarEntityProperties(properties); entityTree->withWriteLock([&] { if (entityTree->updateEntity(id, properties)) { @@ -1834,6 +1893,11 @@ AvatarEntityMap MyAvatar::getAvatarEntityData() const { return data; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring getAvatarEntityData() because don't have canRezAvatarEntities permission on domain"; + return data; + } + QList avatarEntityIDs; _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); @@ -1879,6 +1943,12 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { // avatarEntityData is expected to be a map of QByteArrays that represent EntityItemProperties objects from JavaScript, // aka: unfortunately-formatted-binary-blobs because we store them in non-human-readable format in Settings. // + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring setAvatarEntityData() because don't have canRezAvatarEntities permission on domain"; + return; + } + if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) { // the data is suspect qCDebug(interfaceapp) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size(); @@ -1939,6 +2009,12 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { void MyAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) { // NOTE: this is an invokable Script call + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring updateAvatarEntity() because don't have canRezAvatarEntities permission on domain"; + return; + } + bool changed = false; _avatarEntitiesLock.withWriteLock([&] { auto data = QJsonDocument::fromBinaryData(entityData); @@ -2030,7 +2106,6 @@ void MyAvatar::loadData() { allowAvatarLeaningPreferenceStrings[static_cast(AllowAvatarLeaningPreference::Default)]))); setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); - _follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); setEnableDebugDrawBaseOfSupport(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBaseOfSupport)); setEnableDebugDrawDefaultPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawDefaultPose)); setEnableDebugDrawAnimPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawAnimPose)); @@ -2532,7 +2607,7 @@ void MyAvatar::removeWornAvatarEntity(const EntityItemID& entityID) { auto entity = entityTree->findEntityByID(entityID); if (entity && isWearableEntity(entity)) { treeRenderer->deleteEntity(entityID); - clearAvatarEntity(entityID); + clearAvatarEntityInternal(entityID); } } } @@ -2565,6 +2640,13 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() { QVariantList avatarEntitiesData; auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + + if (entityTree && !DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) + << "Ignoring getAvatarEntitiesVariant() because don't have canRezAvatarEntities permission on domain"; + return avatarEntitiesData; + } + if (entityTree) { QList avatarEntityIDs; _avatarEntitiesLock.withReadLock([&] { @@ -2897,6 +2979,11 @@ void MyAvatar::attach(const QString& modelURL, const QString& jointName, ); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring attach() because don't have canRezAvatarEntities permission on domain"; + return; + } + AttachmentData data; data.modelURL = modelURL; data.jointName = jointName; @@ -2918,6 +3005,11 @@ void MyAvatar::detachOne(const QString& modelURL, const QString& jointName) { ); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring detachOne() because don't have canRezAvatarEntities permission on domain"; + return; + } + QUuid entityID; if (findAvatarEntity(modelURL, jointName, entityID)) { DependencyManager::get()->deleteEntity(entityID); @@ -2933,6 +3025,11 @@ void MyAvatar::detachAll(const QString& modelURL, const QString& jointName) { ); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring detachAll() because don't have canRezAvatarEntities permission on domain"; + return; + } + QUuid entityID; while (findAvatarEntity(modelURL, jointName, entityID)) { DependencyManager::get()->deleteEntity(entityID); @@ -2946,6 +3043,11 @@ void MyAvatar::setAttachmentData(const QVector& attachmentData) Q_ARG(const QVector&, attachmentData)); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring setAttachmentData() because don't have canRezAvatarEntities permission on domain"; + return; + } + std::vector newEntitiesProperties; for (auto& data : attachmentData) { QUuid entityID; @@ -2968,6 +3070,12 @@ void MyAvatar::setAttachmentData(const QVector& attachmentData) QVector MyAvatar::getAttachmentData() const { QVector attachmentData; + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring getAttachmentData() because don't have canRezAvatarEntities permission on domain"; + return attachmentData; + } + QList avatarEntityIDs; _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); @@ -2982,6 +3090,13 @@ QVector MyAvatar::getAttachmentData() const { QVariantList MyAvatar::getAttachmentsVariant() const { QVariantList result; + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) + << "Ignoring getAttachmentsVariant() because don't have canRezAvatarEntities permission on domain"; + return result; + } + for (const auto& attachment : getAttachmentData()) { result.append(attachment.toVariant()); } @@ -2994,6 +3109,13 @@ void MyAvatar::setAttachmentsVariant(const QVariantList& variant) { Q_ARG(const QVariantList&, variant)); return; } + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) + << "Ignoring setAttachmentsVariant() because don't have canRezAvatarEntities permission on domain"; + return; + } + QVector newAttachments; newAttachments.reserve(variant.size()); for (const auto& attachmentVar : variant) { @@ -4065,7 +4187,8 @@ float MyAvatar::getGravity() { void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); - bool sendPackets = !DependencyManager::get()->getSessionUUID().isNull(); + bool sendPackets = !DependencyManager::get()->getSessionUUID().isNull() + && DependencyManager::get()->getThisNodeCanRezAvatarEntities(); if (!sendPackets) { return; } @@ -5514,7 +5637,7 @@ void MyAvatar::setSitStandStateChange(bool stateChanged) { } // Determine if the user's real-world sit/stand state has changed. -float MyAvatar::getSitStandStateChange() const { +bool MyAvatar::getSitStandStateChange() const { return _sitStandStateChange; } @@ -5696,7 +5819,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal_userSitting(const MyAvatar bool stepDetected = false; if (forwardLeanAmount > MAX_FORWARD_LEAN) { stepDetected = true; - } else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) { + } else if (forwardLeanAmount < -MAX_BACKWARD_LEAN) { stepDetected = true; } else { stepDetected = fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 3d278cf983..b4153400a7 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1454,6 +1454,7 @@ public: void removeWornAvatarEntity(const EntityItemID& entityID); void clearWornAvatarEntities(); + bool hasAvatarEntities() const; /**jsdoc * Checks whether your avatar is flying. @@ -1800,7 +1801,7 @@ public: void setAnalogPlusSprintSpeed(float value); float getAnalogPlusSprintSpeed() const; void setSitStandStateChange(bool stateChanged); - float getSitStandStateChange() const; + bool getSitStandStateChange() const; void updateSitStandState(float newHeightReading, float dt); QVector getScriptUrls(); @@ -1939,6 +1940,8 @@ public: void avatarEntityDataToJson(QJsonObject& root) const override; + void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; + /**jsdoc * @comment Uses the base class's JSDoc. */ @@ -2277,12 +2280,6 @@ public slots: */ bool getEnableMeshVisible() const override; - /**jsdoc - * @function MyAvatar.storeAvatarEntityDataPayload - * @deprecated This function is deprecated and will be removed. - */ - void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; - /**jsdoc * @comment Uses the base class's JSDoc. */ @@ -2656,6 +2653,7 @@ private slots: protected: void handleChangedAvatarEntityData(); + void handleCanRezAvatarEntitiesChanged(bool canRezAvatarEntities); virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override; virtual void forgetChild(SpatiallyNestablePointer newChild) const override; virtual void recalculateChildCauterization() const override; @@ -2710,6 +2708,10 @@ private: void attachmentDataToEntityProperties(const AttachmentData& data, EntityItemProperties& properties); AttachmentData entityPropertiesToAttachmentData(const EntityItemProperties& properties) const; bool findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID); + void addAvatarEntitiesToTree(); + + // FIXME: Rename to clearAvatarEntity() once the API call is removed. + void clearAvatarEntityInternal(const QUuid& entityID) override; bool cameraInsideHead(const glm::vec3& cameraPosition) const; @@ -2907,8 +2909,6 @@ private: void setForceActivateVertical(bool val); bool getForceActivateHorizontal() const; void setForceActivateHorizontal(bool val); - bool getToggleHipsFollowing() const; - void setToggleHipsFollowing(bool followHead); std::atomic _forceActivateRotation { false }; std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; @@ -3109,6 +3109,8 @@ private: glm::vec3 _cameraEyesOffset; float _landingAfterJumpTime { 0.0f }; + + QTimer _addAvatarEntitiesToTreeTimer; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 4de353ad0e..9589fc26ff 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -364,7 +364,7 @@ void setupPreferences() { auto preference = new SpinnerSliderPreference(VR_MOVEMENT, "Camera Sensitivity", getter, setter); preference->setMin(0.01f); preference->setMax(5.0f); - preference->setStep(0.1); + preference->setStep(0.1f); preference->setDecimals(2); preferences->addPreference(preference); } diff --git a/libraries/audio/src/AudioHRTF.cpp b/libraries/audio/src/AudioHRTF.cpp index be7fecb450..fa8665d1a9 100644 --- a/libraries/audio/src/AudioHRTF.cpp +++ b/libraries/audio/src/AudioHRTF.cpp @@ -91,8 +91,8 @@ static const float azimuthTable[NAZIMUTH][3] = { // A first-order shelving filter is used to minimize the disturbance in ITD. // // Loosely based on data from S. Spagnol, "Distance rendering and perception of nearby virtual sound sources -// with a near-field filter model,” Applied Acoustics (2017) -// +// with a near-field filter model," Applied Acoustics (2017) +// static const int NNEARFIELD = 9; static const float nearFieldTable[NNEARFIELD][3] = { // { b0, b1, a1 } { 0.008410604f, -0.000262748f, -0.991852144f }, // gain = 1/256 @@ -388,7 +388,12 @@ void crossfade_4x2_AVX2(float* src, float* dst, const float* win, int numFrames) void interpolate_AVX2(const float* src0, const float* src1, float* dst, float frac, float gain); static void FIR_1x4(float* src, float* dst0, float* dst1, float* dst2, float* dst3, float coef[4][HRTF_TAPS], int numFrames) { +#ifndef STACK_PROTECTOR + // Enabling -fstack-protector on gcc causes an undefined reference to FIR_1x4_AVX512 here static auto f = cpuSupportsAVX512() ? FIR_1x4_AVX512 : (cpuSupportsAVX2() ? FIR_1x4_AVX2 : FIR_1x4_SSE); +#else + static auto f = cpuSupportsAVX2() ? FIR_1x4_AVX2 : FIR_1x4_SSE; +#endif (*f)(src, dst0, dst1, dst2, dst3, coef, numFrames); // dispatch } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index f361e15999..7d8b313377 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1444,9 +1444,7 @@ int Avatar::getJointIndex(const QString& name) const { } withValidJointIndicesCache([&]() { - if (_modelJointIndicesCache.contains(name)) { - result = _modelJointIndicesCache.value(name) - 1; - } + result = _modelJointIndicesCache.value(name, result + 1) - 1; }); return result; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index adb7222ee3..c5085e927d 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1938,6 +1938,10 @@ void AvatarData::clearJointsData() { } int AvatarData::getFauxJointIndex(const QString& name) const { + static constexpr QChar fauxJointFirstChar('_');// The first character of all the faux joint names. + if (!name.startsWith(fauxJointFirstChar)) { + return -1; + }; if (name == "_SENSOR_TO_WORLD_MATRIX") { return SENSOR_TO_WORLD_MATRIX_INDEX; } @@ -2246,7 +2250,7 @@ void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType, void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID) { if (traitType == AvatarTraits::AvatarEntity) { - clearAvatarEntity(instanceID); + clearAvatarEntityInternal(instanceID); } else if (traitType == AvatarTraits::Grab) { clearAvatarGrabData(instanceID); } @@ -3034,6 +3038,10 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) { // NOTE: requiresRemovalFromTree is unused + clearAvatarEntityInternal(entityID); +} + +void AvatarData::clearAvatarEntityInternal(const QUuid& entityID) { bool removedEntity = false; _avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] { removedEntity = _packedAvatarEntityData.remove(entityID); @@ -3046,6 +3054,24 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFr } } +void AvatarData::clearAvatarEntities() { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + for (const auto& entityID : avatarEntityIDs) { + clearAvatarEntityInternal(entityID); + } +} + +QList AvatarData::getAvatarEntityIDs() const { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + return avatarEntityIDs; +} + AvatarEntityMap AvatarData::getAvatarEntityData() const { // overridden where needed // NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 2e25c9559c..b25a6938d7 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1186,6 +1186,13 @@ public: * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); + + // FIXME: Rename to clearAvatarEntity() once the API call is removed. + virtual void clearAvatarEntityInternal(const QUuid& entityID); + + void clearAvatarEntities(); + + QList getAvatarEntityIDs() const; /**jsdoc * Enables blend shapes set using {@link Avatar.setBlendshape} or {@link MyAvatar.setBlendshape} to be transmitted to other diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index d70c6586b7..d9b63d6b7a 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -168,20 +168,20 @@ void TextureBaker::processTexture() { gpu::BackendTarget::GLES32 }}; for (auto target : BACKEND_TARGETS) { - auto processedTexture = image::processImage(buffer, _textureURL.toString().toStdString(), image::ColorChannel::NONE, - ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, true, - target, _abortProcessing); - if (!processedTexture) { + auto processedTextureAndSize = image::processImage(buffer, _textureURL.toString().toStdString(), image::ColorChannel::NONE, + ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, true, + target, _abortProcessing); + if (!processedTextureAndSize.first) { handleError("Could not process texture " + _textureURL.toString()); return; } - processedTexture->setSourceHash(hash); + processedTextureAndSize.first->setSourceHash(hash); if (shouldStop()) { return; } - auto memKTX = gpu::Texture::serialize(*processedTexture); + auto memKTX = gpu::Texture::serialize(*processedTextureAndSize.first, processedTextureAndSize.second); if (!memKTX) { handleError("Could not serialize " + _textureURL.toString() + " to KTX"); return; @@ -211,19 +211,19 @@ void TextureBaker::processTexture() { // Uncompressed KTX if (_textureType == image::TextureUsage::Type::SKY_TEXTURE || _textureType == image::TextureUsage::Type::AMBIENT_TEXTURE) { buffer->reset(); - auto processedTexture = image::processImage(std::move(buffer), _textureURL.toString().toStdString(), image::ColorChannel::NONE, - ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, false, gpu::BackendTarget::GL45, _abortProcessing); - if (!processedTexture) { + auto processedTextureAndSize = image::processImage(std::move(buffer), _textureURL.toString().toStdString(), image::ColorChannel::NONE, + ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, false, gpu::BackendTarget::GL45, _abortProcessing); + if (!processedTextureAndSize.first) { handleError("Could not process texture " + _textureURL.toString()); return; } - processedTexture->setSourceHash(hash); + processedTextureAndSize.first->setSourceHash(hash); if (shouldStop()) { return; } - auto memKTX = gpu::Texture::serialize(*processedTexture); + auto memKTX = gpu::Texture::serialize(*processedTextureAndSize.first, processedTextureAndSize.second); if (!memKTX) { handleError("Could not serialize " + _textureURL.toString() + " to KTX"); return; diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index eb4d2dd8c3..3b57bc3a1e 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -78,6 +78,9 @@ public: /// Returns a pointer to the underlying socket, to which WebSocket message bodies should be written. QTcpSocket* socket() const { return _socket; } + /// Returns the IP address on the other side of the connection + const QHostAddress &peerAddress() const { return _address; } + /// Returns the request operation. QNetworkAccessManager::Operation requestOperation() const { return _requestOperation; } diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 3e13b301df..b7cfcde224 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -29,6 +29,8 @@ #include "RenderableZoneEntityItem.h" #include "RenderableMaterialEntityItem.h" +#include "RenderPipelines.h" + using namespace render; using namespace render::entities; @@ -149,10 +151,11 @@ Item::Bound EntityRenderer::getBound(RenderArgs* args) { } ShapeKey EntityRenderer::getShapeKey() { + ShapeKey::Builder builder = ShapeKey::Builder().withOwnPipeline(); if (_primitiveMode == PrimitiveMode::LINES) { - return ShapeKey::Builder().withOwnPipeline().withWireframe(); + builder.withWireframe(); } - return ShapeKey::Builder().withOwnPipeline(); + return builder.build(); } render::hifi::Tag EntityRenderer::getTagMask() const { @@ -365,6 +368,7 @@ bool EntityRenderer::needsRenderUpdate() const { if (_prevIsTransparent != isTransparent()) { return true; } + return needsRenderUpdateFromEntity(_entity); } @@ -491,6 +495,176 @@ void EntityRenderer::removeMaterial(graphics::MaterialPointer material, const st emit requestRenderUpdate(); } +EntityRenderer::Pipeline EntityRenderer::getPipelineType(const graphics::MultiMaterial& materials) { + if (materials.top().material && materials.top().material->isProcedural() && materials.top().material->isReady()) { + return Pipeline::PROCEDURAL; + } + + graphics::MaterialKey drawMaterialKey = materials.getMaterialKey(); + if (drawMaterialKey.isEmissive() || drawMaterialKey.isMetallic() || drawMaterialKey.isScattering()) { + return Pipeline::MATERIAL; + } + + // If the material is using any map, we need to use a material ShapeKey + for (int i = 0; i < graphics::Material::MapChannel::NUM_MAP_CHANNELS; i++) { + if (drawMaterialKey.isMapChannel(graphics::Material::MapChannel(i))) { + return Pipeline::MATERIAL; + } + } + return Pipeline::SIMPLE; +} + +bool EntityRenderer::needsRenderUpdateFromMaterials() const { + MaterialMap::const_iterator materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials.find("0"); + + if (materials == _materials.cend()) { + return false; + } + } + + if (materials->second.shouldUpdate()) { + return true; + } + + if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) { + auto procedural = std::static_pointer_cast(materials->second.top().material); + if (procedural->isFading()) { + return true; + } + } + + return false; +} + +void EntityRenderer::updateMaterials(bool baseMaterialChanged) { + MaterialMap::iterator materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials.find("0"); + + if (materials == _materials.end()) { + return; + } + } + + if (baseMaterialChanged) { + materials->second.setNeedsUpdate(true); + } + + bool requestUpdate = false; + if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) { + auto procedural = std::static_pointer_cast(materials->second.top().material); + if (procedural->isFading()) { + procedural->setIsFading(Interpolate::calculateFadeRatio(procedural->getFadeStartTime()) < 1.0f); + requestUpdate = true; + } + } + + if (materials->second.shouldUpdate()) { + RenderPipelines::updateMultiMaterial(materials->second); + requestUpdate = true; + } + + if (requestUpdate) { + emit requestRenderUpdate(); + } +} + +bool EntityRenderer::materialsTransparent() const { + MaterialMap::const_iterator materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials.find("0"); + + if (materials == _materials.cend()) { + return false; + } + } + + if (materials->second.top().material) { + if (materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) { + auto procedural = std::static_pointer_cast(materials->second.top().material); + if (procedural->isFading()) { + return true; + } + } + + if (materials->second.getMaterialKey().isTranslucent()) { + return true; + } + } + + return false; +} + +Item::Bound EntityRenderer::getMaterialBound(RenderArgs* args) { + MaterialMap::iterator materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials.find("0"); + + if (materials == _materials.end()) { + return EntityRenderer::getBound(args); + } + } + + if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) { + auto procedural = std::static_pointer_cast(materials->second.top().material); + if (procedural->hasVertexShader() && procedural->hasBoundOperator()) { + return procedural->getBound(args); + } + } + + return EntityRenderer::getBound(args); +} + +void EntityRenderer::updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& builder) { + MaterialMap::iterator materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials.find("0"); + if (materials != _materials.end()) { + if (materials->second.shouldUpdate()) { + RenderPipelines::updateMultiMaterial(materials->second); + } + } else { + return; + } + } + + if (isTransparent()) { + builder.withTranslucent(); + } + + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + + builder.withCullFaceMode(materials->second.getCullFaceMode()); + + graphics::MaterialKey drawMaterialKey = materials->second.getMaterialKey(); + if (drawMaterialKey.isUnlit()) { + builder.withUnlit(); + } + + auto pipelineType = getPipelineType(materials->second); + if (pipelineType == Pipeline::MATERIAL) { + builder.withMaterial(); + + if (drawMaterialKey.isNormalMap()) { + builder.withTangents(); + } + if (drawMaterialKey.isLightMap()) { + builder.withLightMap(); + } + } else if (pipelineType == Pipeline::PROCEDURAL) { + builder.withOwnPipeline(); + } +} + glm::vec4 EntityRenderer::calculatePulseColor(const glm::vec4& color, const PulsePropertyGroup& pulseProperties, quint64 start) { if (pulseProperties.getPeriod() == 0.0f || (pulseProperties.getColorMode() == PulseMode::NONE && pulseProperties.getAlphaMode() == PulseMode::NONE)) { return color; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 9392d61e75..2731fe6a50 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -55,8 +55,14 @@ public: const uint64_t& getUpdateTime() const { return _updateTime; } + enum class Pipeline { + SIMPLE, + MATERIAL, + PROCEDURAL + }; virtual void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName); virtual void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName); + static Pipeline getPipelineType(const graphics::MultiMaterial& materials); virtual scriptable::ScriptableModelBase getScriptableModel() override { return scriptable::ScriptableModelBase(); } @@ -117,6 +123,14 @@ protected: Transform getTransformToCenterWithMaybeOnlyLocalRotation(const EntityItemPointer& entity, bool& success) const; + // Shared methods for entities that support materials + using MaterialMap = std::unordered_map; + bool needsRenderUpdateFromMaterials() const; + void updateMaterials(bool baseMaterialChanged = false); + bool materialsTransparent() const; + Item::Bound getMaterialBound(RenderArgs* args); + void updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& builder); + Item::Bound _bound; SharedSoundPointer _collisionSound; QUuid _changeHandlerId; @@ -132,13 +146,13 @@ protected: RenderLayer _renderLayer { RenderLayer::WORLD }; PrimitiveMode _primitiveMode { PrimitiveMode::SOLID }; QVector _renderWithZones; - BillboardMode _billboardMode; + BillboardMode _billboardMode { BillboardMode::NONE }; bool _cauterized { false }; bool _moving { false }; Transform _renderTransform; - std::unordered_map _materials; - std::mutex _materialsLock; + MaterialMap _materials; + mutable std::mutex _materialsLock; quint64 _created; diff --git a/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp b/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp index bf005ae2e5..42df1e2888 100644 --- a/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp @@ -11,10 +11,15 @@ #include #include +#include "RenderPipelines.h" + using namespace render; using namespace render::entities; -GizmoEntityRenderer::GizmoEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {} +GizmoEntityRenderer::GizmoEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { + _material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE); + addMaterial(graphics::MaterialLayer(_material, 0), "0"); +} GizmoEntityRenderer::~GizmoEntityRenderer() { auto geometryCache = DependencyManager::get(); @@ -31,12 +36,8 @@ GizmoEntityRenderer::~GizmoEntityRenderer() { } } -bool GizmoEntityRenderer::isTransparent() const { - bool ringTransparent = _gizmoType == GizmoType::RING && (_ringProperties.getInnerStartAlpha() < 1.0f || - _ringProperties.getInnerEndAlpha() < 1.0f || _ringProperties.getOuterStartAlpha() < 1.0f || - _ringProperties.getOuterEndAlpha() < 1.0f); - - return Parent::isTransparent() || ringTransparent; +bool GizmoEntityRenderer::needsRenderUpdate() const { + return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate(); } void GizmoEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { @@ -193,10 +194,20 @@ void GizmoEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint } } } + + updateMaterials(); +} + +bool GizmoEntityRenderer::isTransparent() const { + bool ringTransparent = _gizmoType == GizmoType::RING && (_ringProperties.getInnerStartAlpha() < 1.0f || + _ringProperties.getInnerEndAlpha() < 1.0f || _ringProperties.getOuterStartAlpha() < 1.0f || + _ringProperties.getOuterEndAlpha() < 1.0f); + + return ringTransparent || Parent::isTransparent() || materialsTransparent(); } Item::Bound GizmoEntityRenderer::getBound(RenderArgs* args) { - auto bound = Parent::getBound(args); + auto bound = Parent::getMaterialBound(args); if (_ringProperties.getHasTickMarks()) { glm::vec3 scale = bound.getScale(); for (int i = 0; i < 3; i += 2) { @@ -220,13 +231,8 @@ Item::Bound GizmoEntityRenderer::getBound(RenderArgs* args) { } ShapeKey GizmoEntityRenderer::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace(); - if (isTransparent()) { - builder.withTranslucent(); - } - if (_primitiveMode == PrimitiveMode::LINES) { - builder.withUnlit().withDepthBias(); - } + auto builder = render::ShapeKey::Builder().withDepthBias(); + updateShapeKeyBuilderFromMaterials(builder); return builder.build(); } @@ -249,15 +255,29 @@ void GizmoEntityRenderer::doRender(RenderArgs* args) { transparent = isTransparent(); }); - bool wireframe = render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES; - bool forward = _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD; + graphics::MultiMaterial materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials["0"]; + } - geometryCache->bindSimpleProgram(batch, false, transparent, wireframe, true, true, forward, graphics::MaterialKey::CULL_NONE); + bool wireframe = render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES; transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode, args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition(), true)); batch.setModelTransform(transform); + Pipeline pipelineType = getPipelineType(materials); + if (pipelineType == Pipeline::PROCEDURAL) { + auto procedural = std::static_pointer_cast(materials.top().material); + transparent |= procedural->isFading(); + procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(transparent)); + } else if (pipelineType == Pipeline::MATERIAL) { + if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) { + args->_details._materialSwitches++; + } + } + // Background circle geometryCache->renderVertices(batch, wireframe ? gpu::LINE_STRIP : _solidPrimitive, _ringGeometryID); diff --git a/libraries/entities-renderer/src/RenderableGizmoEntityItem.h b/libraries/entities-renderer/src/RenderableGizmoEntityItem.h index 6a09d1a047..b6c22b19de 100644 --- a/libraries/entities-renderer/src/RenderableGizmoEntityItem.h +++ b/libraries/entities-renderer/src/RenderableGizmoEntityItem.h @@ -13,6 +13,8 @@ #include +#include + namespace render { namespace entities { class GizmoEntityRenderer : public TypedEntityRenderer { @@ -29,10 +31,12 @@ protected: bool isTransparent() const override; private: + virtual bool needsRenderUpdate() const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual void doRender(RenderArgs* args) override; + std::shared_ptr _material { std::make_shared() }; GizmoType _gizmoType { UNSET_GIZMO_TYPE }; RingGizmoPropertyGroup _ringProperties; PrimitiveMode _prevPrimitiveMode; diff --git a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp index e03655f09c..3fa2174114 100644 --- a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp @@ -10,12 +10,17 @@ #include #include +#include + +#include "RenderPipelines.h" using namespace render; using namespace render::entities; ImageEntityRenderer::ImageEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { _geometryId = DependencyManager::get()->allocateID(); + _material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE); + addMaterial(graphics::MaterialLayer(_material, 0), "0"); } ImageEntityRenderer::~ImageEntityRenderer() { @@ -25,8 +30,8 @@ ImageEntityRenderer::~ImageEntityRenderer() { } } -bool ImageEntityRenderer::isTransparent() const { - return Parent::isTransparent() || (_textureIsLoaded && _texture->getGPUTexture() && _texture->getGPUTexture()->getUsage().isAlpha()) || _alpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; +bool ImageEntityRenderer::needsRenderUpdate() const { + return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate(); } void ImageEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { @@ -51,67 +56,111 @@ void ImageEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint _textureIsLoaded = false; } - _emissive = entity->getEmissive(); _keepAspectRatio = entity->getKeepAspectRatio(); _subImage = entity->getSubImage(); - - _color = entity->getColor(); - _alpha = entity->getAlpha(); _pulseProperties = entity->getPulseProperties(); + bool materialChanged = false; + glm::vec3 color = toGlm(entity->getColor()); + if (_color != color) { + _color = color; + _material->setAlbedo(color); + materialChanged = true; + } + + float alpha = entity->getAlpha(); + if (_alpha != alpha) { + _alpha = alpha; + _material->setOpacity(alpha); + materialChanged = true; + } + + auto emissive = entity->getEmissive(); + if (_emissive != emissive) { + _emissive = emissive; + _material->setUnlit(_emissive); + materialChanged = true; + } + + updateMaterials(materialChanged); + + bool nextTextureLoaded = _texture && (_texture->isLoaded() || _texture->isFailed()); if (!_textureIsLoaded) { emit requestRenderUpdate(); + if (nextTextureLoaded) { + float width = _texture->getOriginalWidth(); + float height = _texture->getOriginalHeight(); + glm::vec3 naturalDimensions = glm::vec3(1.0f, 1.0f, 0.01f); + if (width < height) { + naturalDimensions.x = width / height; + } else { + naturalDimensions.y = height / width; + } + // Unlike Models (where the Renderer also doubles as the EntityItem), Images need to + // convey this information back to the game object from the Renderer + entity->setNaturalDimension(naturalDimensions); + } } - _textureIsLoaded = _texture && (_texture->isLoaded() || _texture->isFailed()); + _textureIsLoaded = nextTextureLoaded; +} + +bool ImageEntityRenderer::isTransparent() const { + bool imageTransparent = _alpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE || + (_textureIsLoaded && _texture->getGPUTexture() && _texture->getGPUTexture()->getUsage().isAlpha()); + return imageTransparent || Parent::isTransparent() || materialsTransparent(); +} + +Item::Bound ImageEntityRenderer::getBound(RenderArgs* args) { + return Parent::getMaterialBound(args); } ShapeKey ImageEntityRenderer::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace().withDepthBias(); - if (isTransparent()) { - builder.withTranslucent(); - } - - if (_emissive) { - builder.withUnlit(); - } - - if (_primitiveMode == PrimitiveMode::LINES) { - builder.withWireframe(); - } - + auto builder = render::ShapeKey::Builder().withDepthBias(); + updateShapeKeyBuilderFromMaterials(builder); return builder.build(); } void ImageEntityRenderer::doRender(RenderArgs* args) { - glm::vec4 color = glm::vec4(toGlm(_color), _alpha); - color = EntityRenderer::calculatePulseColor(color, _pulseProperties, _created); - Transform transform; - withReadLock([&] { - transform = _renderTransform; - }); + PerformanceTimer perfTimer("RenderableImageEntityItem::render"); + Q_ASSERT(args->_batch); - if (!_visible || !_texture || !_texture->isLoaded() || color.a == 0.0f) { + graphics::MultiMaterial materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials["0"]; + } + + auto& schema = materials.getSchemaBuffer().get(); + glm::vec4 color = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity); + color = EntityRenderer::calculatePulseColor(color, _pulseProperties, _created); + + if (!_texture || !_texture->isLoaded() || color.a == 0.0f) { return; } - Q_ASSERT(args->_batch); + Transform transform; + bool transparent; + withReadLock([&] { + transform = _renderTransform; + transparent = isTransparent(); + }); + gpu::Batch* batch = args->_batch; transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode, args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition())); - batch->setModelTransform(transform); - batch->setResourceTexture(0, _texture->getGPUTexture()); - float imageWidth = _texture->getWidth(); float imageHeight = _texture->getHeight(); + float originalWidth = _texture->getOriginalWidth(); + float originalHeight = _texture->getOriginalHeight(); QRect fromImage; if (_subImage.width() <= 0) { fromImage.setX(0); fromImage.setWidth(imageWidth); } else { - float scaleX = imageWidth / _texture->getOriginalWidth(); + float scaleX = imageWidth / originalWidth; fromImage.setX(scaleX * _subImage.x()); fromImage.setWidth(scaleX * _subImage.width()); } @@ -120,22 +169,48 @@ void ImageEntityRenderer::doRender(RenderArgs* args) { fromImage.setY(0); fromImage.setHeight(imageHeight); } else { - float scaleY = imageHeight / _texture->getOriginalHeight(); + float scaleY = imageHeight / originalHeight; fromImage.setY(scaleY * _subImage.y()); fromImage.setHeight(scaleY * _subImage.height()); } - float maxSize = glm::max(fromImage.width(), fromImage.height()); - float x = _keepAspectRatio ? fromImage.width() / (2.0f * maxSize) : 0.5f; - float y = _keepAspectRatio ? fromImage.height() / (2.0f * maxSize) : 0.5f; - glm::vec2 texCoordBottomLeft((fromImage.x() + 0.5f) / imageWidth, (fromImage.y() + fromImage.height() - 0.5f) / imageHeight); glm::vec2 texCoordTopRight((fromImage.x() + fromImage.width() - 0.5f) / imageWidth, (fromImage.y() + 0.5f) / imageHeight); + if (_keepAspectRatio) { + glm::vec3 scale = transform.getScale(); + float targetAspectRatio = originalWidth / originalHeight; + float currentAspectRatio = scale.x / scale.y; + + if (targetAspectRatio < currentAspectRatio) { + scale.x *= targetAspectRatio / currentAspectRatio; + } else { + scale.y /= targetAspectRatio / currentAspectRatio; + } + transform.setScale(scale); + } + batch->setModelTransform(transform); + + Pipeline pipelineType = getPipelineType(materials); + if (pipelineType == Pipeline::PROCEDURAL) { + auto procedural = std::static_pointer_cast(materials.top().material); + transparent |= procedural->isFading(); + procedural->prepare(*batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(transparent)); + } else if (pipelineType == Pipeline::SIMPLE) { + batch->setResourceTexture(0, _texture->getGPUTexture()); + } else { + if (RenderPipelines::bindMaterials(materials, *batch, args->_renderMode, args->_enableTexturing)) { + args->_details._materialSwitches++; + } + } + DependencyManager::get()->renderQuad( - *batch, glm::vec2(-x, -y), glm::vec2(x, y), texCoordBottomLeft, texCoordTopRight, + *batch, glm::vec2(-0.5f), glm::vec2(0.5f), texCoordBottomLeft, texCoordTopRight, color, _geometryId ); - batch->setResourceTexture(0, nullptr); + if (pipelineType == Pipeline::SIMPLE) { + // we have to reset this to white for other simple shapes + batch->setResourceTexture(graphics::slot::texture::Texture::MaterialAlbedo, DependencyManager::get()->getWhiteTexture()); + } } diff --git a/libraries/entities-renderer/src/RenderableImageEntityItem.h b/libraries/entities-renderer/src/RenderableImageEntityItem.h index 2359dcc6d1..ff82dc157a 100644 --- a/libraries/entities-renderer/src/RenderableImageEntityItem.h +++ b/libraries/entities-renderer/src/RenderableImageEntityItem.h @@ -13,6 +13,8 @@ #include +#include + namespace render { namespace entities { class ImageEntityRenderer : public TypedEntityRenderer { @@ -23,11 +25,13 @@ public: ~ImageEntityRenderer(); protected: + Item::Bound getBound(RenderArgs* args) override; ShapeKey getShapeKey() override; bool isTransparent() const override; private: + virtual bool needsRenderUpdate() const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual void doRender(RenderArgs* args) override; @@ -40,8 +44,9 @@ private: bool _keepAspectRatio; QRect _subImage; - glm::u8vec3 _color; - float _alpha; + std::shared_ptr _material { std::make_shared() }; + glm::vec3 _color { NAN }; + float _alpha { NAN }; PulsePropertyGroup _pulseProperties; int _geometryId { 0 }; diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 714defe817..8f8a2076f8 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -219,6 +219,10 @@ ShapeKey MaterialEntityRenderer::getShapeKey() { builder.withTranslucent(); } + if (drawMaterial) { + builder.withCullFaceMode(drawMaterial->getCullFaceMode()); + } + if (drawMaterial && drawMaterial->isProcedural() && drawMaterial->isReady()) { builder.withOwnPipeline(); } else { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 3c245e5d4f..69bd752395 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1254,13 +1254,13 @@ void ModelEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint if (_hasModel && !model) { model = std::make_shared(nullptr, entity.get(), _created); connect(model.get(), &Model::requestRenderUpdate, this, &ModelEntityRenderer::requestRenderUpdate); - connect(model.get(), &Model::setURLFinished, this, [&](bool didVisualGeometryRequestSucceed) { + connect(model.get(), &Model::setURLFinished, this, [=](bool didVisualGeometryRequestSucceed) { _didLastVisualGeometryRequestSucceed = didVisualGeometryRequestSucceed; const render::ScenePointer& scene = AbstractViewStateInterface::instance()->getMain3DScene(); render::Transaction transaction; - transaction.updateItem(_renderItemID, [&](PayloadProxyInterface& self) { + transaction.updateItem(_renderItemID, [=](PayloadProxyInterface& self) { const render::ScenePointer& scene = AbstractViewStateInterface::instance()->getMain3DScene(); - withWriteLock([&] { + withWriteLock([=] { setKey(didVisualGeometryRequestSucceed, _model); _model->setVisibleInScene(_visible, scene); _model->setCauterized(_cauterized, scene); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 35739c2430..a6fee03311 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -29,26 +29,7 @@ ShapeEntityRenderer::ShapeEntityRenderer(const EntityItemPointer& entity) : Pare } bool ShapeEntityRenderer::needsRenderUpdate() const { - if (resultWithReadLock([&] { - auto mat = _materials.find("0"); - if (mat != _materials.end() && mat->second.top().material && mat->second.top().material->isProcedural() && - mat->second.top().material->isReady()) { - auto procedural = std::static_pointer_cast(mat->second.top().material); - if (procedural->isFading()) { - return true; - } - } - - if (mat != _materials.end() && mat->second.shouldUpdate()) { - return true; - } - - return false; - })) { - return true; - } - - return Parent::needsRenderUpdate(); + return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate(); } void ShapeEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { @@ -91,155 +72,58 @@ void ShapeEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint materialChanged = true; } - withReadLock([&] { - auto materials = _materials.find("0"); - if (materials != _materials.end()) { - if (materialChanged) { - materials->second.setNeedsUpdate(true); - } - - bool requestUpdate = false; - if (materials->second.top().material && materials->second.top().material->isProcedural() && materials->second.top().material->isReady()) { - auto procedural = std::static_pointer_cast(materials->second.top().material); - if (procedural->isFading()) { - procedural->setIsFading(Interpolate::calculateFadeRatio(procedural->getFadeStartTime()) < 1.0f); - requestUpdate = true; - } - } - - if (materials->second.shouldUpdate()) { - RenderPipelines::updateMultiMaterial(materials->second); - requestUpdate = true; - } - - if (requestUpdate) { - emit requestRenderUpdate(); - } - } - }); + updateMaterials(materialChanged); } bool ShapeEntityRenderer::isTransparent() const { - if (_pulseProperties.getAlphaMode() != PulseMode::NONE) { - return true; - } - - auto mat = _materials.find("0"); - if (mat != _materials.end() && mat->second.top().material) { - if (mat->second.top().material->isProcedural() && mat->second.top().material->isReady()) { - auto procedural = std::static_pointer_cast(mat->second.top().material); - if (procedural->isFading()) { - return true; - } - } - - if (mat->second.getMaterialKey().isTranslucent()) { - return true; - } - } - - return Parent::isTransparent(); + return _pulseProperties.getAlphaMode() != PulseMode::NONE || Parent::isTransparent() || materialsTransparent(); } -ShapeEntityRenderer::Pipeline ShapeEntityRenderer::getPipelineType(const graphics::MultiMaterial& materials) const { - if (materials.top().material && materials.top().material->isProcedural() && materials.top().material->isReady()) { - return Pipeline::PROCEDURAL; - } - - graphics::MaterialKey drawMaterialKey = materials.getMaterialKey(); - if (drawMaterialKey.isEmissive() || drawMaterialKey.isUnlit() || drawMaterialKey.isMetallic() || drawMaterialKey.isScattering()) { - return Pipeline::MATERIAL; - } - - // If the material is using any map, we need to use a material ShapeKey - for (int i = 0; i < graphics::Material::MapChannel::NUM_MAP_CHANNELS; i++) { - if (drawMaterialKey.isMapChannel(graphics::Material::MapChannel(i))) { - return Pipeline::MATERIAL; - } - } - return Pipeline::SIMPLE; +Item::Bound ShapeEntityRenderer::getBound(RenderArgs* args) { + return Parent::getMaterialBound(args); } ShapeKey ShapeEntityRenderer::getShapeKey() { ShapeKey::Builder builder; - auto mat = _materials.find("0"); - if (mat != _materials.end() && mat->second.shouldUpdate()) { - RenderPipelines::updateMultiMaterial(mat->second); - } - - if (isTransparent()) { - builder.withTranslucent(); - } - - if (_primitiveMode == PrimitiveMode::LINES) { - builder.withWireframe(); - } - - auto pipelineType = getPipelineType(mat->second); - if (pipelineType == Pipeline::MATERIAL) { - builder.withMaterial(); - - graphics::MaterialKey drawMaterialKey = mat->second.getMaterialKey(); - if (drawMaterialKey.isNormalMap()) { - builder.withTangents(); - } - if (drawMaterialKey.isLightMap()) { - builder.withLightMap(); - } - if (drawMaterialKey.isUnlit()) { - builder.withUnlit(); - } - builder.withCullFaceMode(mat->second.getCullFaceMode()); - } else if (pipelineType == Pipeline::PROCEDURAL) { - builder.withOwnPipeline(); - } - + updateShapeKeyBuilderFromMaterials(builder); return builder.build(); } -Item::Bound ShapeEntityRenderer::getBound(RenderArgs* args) { - auto mat = _materials.find("0"); - if (mat != _materials.end() && mat->second.top().material && mat->second.top().material->isProcedural() && - mat->second.top().material->isReady()) { - auto procedural = std::static_pointer_cast(mat->second.top().material); - if (procedural->hasVertexShader() && procedural->hasBoundOperator()) { - return procedural->getBound(args); - } - } - return Parent::getBound(args); -} - void ShapeEntityRenderer::doRender(RenderArgs* args) { PerformanceTimer perfTimer("RenderableShapeEntityItem::render"); Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - graphics::MultiMaterial materials; - auto geometryCache = DependencyManager::get(); - GeometryCache::Shape geometryShape = geometryCache->getShapeForEntityShape(_shape); - glm::vec4 outColor; - Pipeline pipelineType; - Transform transform; - withReadLock([&] { - transform = _renderTransform; + { + std::lock_guard lock(_materialsLock); materials = _materials["0"]; - pipelineType = getPipelineType(materials); - auto& schema = materials.getSchemaBuffer().get(); - outColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity); - }); + } + auto& schema = materials.getSchemaBuffer().get(); + glm::vec4 outColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity); outColor = EntityRenderer::calculatePulseColor(outColor, _pulseProperties, _created); if (outColor.a == 0.0f) { return; } + gpu::Batch& batch = *args->_batch; + + auto geometryCache = DependencyManager::get(); + GeometryCache::Shape geometryShape = geometryCache->getShapeForEntityShape(_shape); + Transform transform; + withReadLock([&] { + transform = _renderTransform; + }); + + bool wireframe = render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES; + transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode, args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition(), _shape < entity::Shape::Cube || _shape > entity::Shape::Icosahedron)); batch.setModelTransform(transform); + Pipeline pipelineType = getPipelineType(materials); if (pipelineType == Pipeline::PROCEDURAL) { auto procedural = std::static_pointer_cast(materials.top().material); outColor = procedural->getColor(outColor); @@ -248,7 +132,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(outColor.a < 1.0f)); }); - if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { + if (wireframe) { geometryCache->renderWireShape(batch, geometryShape, outColor); } else { geometryCache->renderShape(batch, geometryShape, outColor); @@ -256,12 +140,21 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { } else if (pipelineType == Pipeline::SIMPLE) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - render::ShapePipelinePointer pipeline = geometryCache->getShapePipelinePointer(outColor.a < 1.0f, false, - _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD, materials.top().material->getCullFaceMode()); - if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { - geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); + bool forward = _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD; + if (outColor.a >= 1.0f) { + render::ShapePipelinePointer pipeline = geometryCache->getShapePipelinePointer(false, wireframe || materials.top().material->isUnlit(), + forward, materials.top().material->getCullFaceMode()); + if (wireframe) { + geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); + } else { + geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); + } } else { - geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); + if (wireframe) { + geometryCache->renderWireShape(batch, geometryShape, outColor); + } else { + geometryCache->renderShape(batch, geometryShape, outColor); + } } } else { if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) { diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index 403d389378..686014f4de 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -35,9 +35,6 @@ private: virtual void doRender(RenderArgs* args) override; virtual bool isTransparent() const override; - enum Pipeline { SIMPLE, MATERIAL, PROCEDURAL }; - Pipeline getPipelineType(const graphics::MultiMaterial& materials) const; - QString _proceduralData; entity::Shape _shape { entity::Sphere }; diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 4248d3f2cd..2858e12afd 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -20,6 +20,7 @@ #include "GLMHelpers.h" #include "DeferredLightingEffect.h" +#include "RenderPipelines.h" using namespace render; using namespace render::entities; @@ -35,6 +36,8 @@ TextEntityRenderer::TextEntityRenderer(const EntityItemPointer& entity) : if (geometryCache) { _geometryID = geometryCache->allocateID(); } + _material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE); + addMaterial(graphics::MaterialLayer(_material, 0), "0"); } TextEntityRenderer::~TextEntityRenderer() { @@ -44,41 +47,8 @@ TextEntityRenderer::~TextEntityRenderer() { } } -bool TextEntityRenderer::isTransparent() const { - return Parent::isTransparent() || _backgroundAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; -} - -bool TextEntityRenderer::isTextTransparent() const { - return resultWithReadLock([&] { - return Parent::isTransparent() || _textAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; - }); -} - -ItemKey TextEntityRenderer::getKey() { - return ItemKey::Builder(Parent::getKey()).withMetaCullGroup(); -} - -ShapeKey TextEntityRenderer::getShapeKey() { - auto builder = render::ShapeKey::Builder().withoutCullFace(); - if (isTransparent()) { - builder.withTranslucent(); - } - if (_unlit) { - builder.withUnlit(); - } - if (_primitiveMode == PrimitiveMode::LINES) { - builder.withWireframe(); - } - return builder.build(); -} - -uint32_t TextEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) const { - auto parentSubs = Parent::metaFetchMetaSubItems(subItems); - if (Item::isValidID(_textRenderID)) { - subItems.emplace_back(_textRenderID); - return parentSubs + 1; - } - return parentSubs; +bool TextEntityRenderer::needsRenderUpdate() const { + return needsRenderUpdateFromMaterials() || Parent::needsRenderUpdate(); } void TextEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { @@ -98,58 +68,126 @@ void TextEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointe _lineHeight = entity->getLineHeight(); _textColor = toGlm(entity->getTextColor()); _textAlpha = entity->getTextAlpha(); - _backgroundColor = toGlm(entity->getBackgroundColor()); - _backgroundAlpha = entity->getBackgroundAlpha(); _leftMargin = entity->getLeftMargin(); _rightMargin = entity->getRightMargin(); _topMargin = entity->getTopMargin(); _bottomMargin = entity->getBottomMargin(); - _unlit = entity->getUnlit(); _font = entity->getFont(); _effect = entity->getTextEffect(); _effectColor = toGlm(entity->getTextEffectColor()); _effectThickness = entity->getTextEffectThickness(); _alignment = entity->getAlignment(); + + bool materialChanged = false; + glm::vec3 color = toGlm(entity->getBackgroundColor()); + if (_backgroundColor != color) { + _backgroundColor = color; + _material->setAlbedo(color); + materialChanged = true; + } + + float alpha = entity->getBackgroundAlpha(); + if (_backgroundAlpha != alpha) { + _backgroundAlpha = alpha; + _material->setOpacity(alpha); + materialChanged = true; + } + + auto unlit = entity->getUnlit(); + if (_unlit != unlit) { + _unlit = unlit; + _material->setUnlit(_unlit); + materialChanged = true; + } + + updateMaterials(materialChanged); + updateTextRenderItem(); } +bool TextEntityRenderer::isTransparent() const { + bool backgroundTransparent = _backgroundAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; + return backgroundTransparent || Parent::isTransparent() || materialsTransparent(); +} + +bool TextEntityRenderer::isTextTransparent() const { + return Parent::isTransparent() || _textAlpha < 1.0f || _pulseProperties.getAlphaMode() != PulseMode::NONE; +} + +Item::Bound TextEntityRenderer::getBound(RenderArgs* args) { + return Parent::getMaterialBound(args); +} + +ItemKey TextEntityRenderer::getKey() { + return ItemKey::Builder(Parent::getKey()).withMetaCullGroup(); +} + +ShapeKey TextEntityRenderer::getShapeKey() { + auto builder = render::ShapeKey::Builder().withDepthBias(); + updateShapeKeyBuilderFromMaterials(builder); + return builder.build(); +} + +uint32_t TextEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) const { + auto parentSubs = Parent::metaFetchMetaSubItems(subItems); + if (Item::isValidID(_textRenderID)) { + subItems.emplace_back(_textRenderID); + return parentSubs + 1; + } + return parentSubs; +} + void TextEntityRenderer::doRender(RenderArgs* args) { PerformanceTimer perfTimer("RenderableTextEntityItem::render"); Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - glm::vec4 backgroundColor; - Transform transform; - withReadLock([&] { - transform = _renderTransform; + graphics::MultiMaterial materials; + { + std::lock_guard lock(_materialsLock); + materials = _materials["0"]; + } - float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - backgroundColor = glm::vec4(_backgroundColor, fadeRatio * _backgroundAlpha); - }); + auto& schema = materials.getSchemaBuffer().get(); + glm::vec4 backgroundColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity); backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created); if (backgroundColor.a <= 0.0f) { return; } + gpu::Batch& batch = *args->_batch; + + bool transparent; + Transform transform; + withReadLock([&] { + transparent = isTransparent(); + transform = _renderTransform; + }); + transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode, args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE ? BillboardModeHelpers::getPrimaryViewFrustumPosition() : args->getViewFrustum().getPosition())); batch.setModelTransform(transform); + Pipeline pipelineType = getPipelineType(materials); + if (pipelineType == Pipeline::PROCEDURAL) { + auto procedural = std::static_pointer_cast(materials.top().material); + transparent |= procedural->isFading(); + procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(transparent)); + } else if (pipelineType == Pipeline::MATERIAL) { + if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) { + args->_details._materialSwitches++; + } + } + auto geometryCache = DependencyManager::get(); - // FIXME: we want to use instanced rendering here, but if textAlpha < 1 and backgroundAlpha < 1, the transparency sorting will be wrong - //render::ShapePipelinePointer pipeline = geometryCache->getShapePipelinePointer(backgroundColor.a < 1.0f, _unlit, - // _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD); - //if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { - // geometryCache->renderWireShapeInstance(args, batch, GeometryCache::Quad, backgroundColor, pipeline); - //} else { - // geometryCache->renderSolidShapeInstance(args, batch, GeometryCache::Quad, backgroundColor, pipeline); - //} + if (pipelineType == Pipeline::SIMPLE) { + geometryCache->renderQuad(batch, glm::vec2(-0.5f), glm::vec2(0.5f), backgroundColor, _geometryID); + } else { + geometryCache->renderQuad(batch, glm::vec2(-0.5f), glm::vec2(0.5f), glm::vec2(0.0f), glm::vec2(1.0f), backgroundColor, _geometryID); + } - geometryCache->renderQuad(batch, glm::vec2(-0.5), glm::vec2(0.5), backgroundColor, _geometryID); - - const int TRIANBLES_PER_QUAD = 2; - args->_details._trianglesRendered += TRIANBLES_PER_QUAD; + const int TRIANGLES_PER_QUAD = 2; + args->_details._trianglesRendered += TRIANGLES_PER_QUAD; } QSizeF TextEntityRenderer::textSize(const QString& text) const { diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.h b/libraries/entities-renderer/src/RenderableTextEntityItem.h index c4cfaab9a2..0dea260763 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.h +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.h @@ -14,6 +14,8 @@ #include "RenderableEntityItem.h" +#include + class TextEntityItem; class TextRenderer3D; @@ -33,6 +35,7 @@ public: protected: bool isTransparent() const override; bool isTextTransparent() const; + Item::Bound getBound(RenderArgs* args) override; ShapeKey getShapeKey() override; ItemKey getKey() override; virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) const override; @@ -41,6 +44,7 @@ protected: void onRemoveFromSceneTyped(const TypedEntityPointer& entity) override; private: + virtual bool needsRenderUpdate() const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual void doRender(RenderArgs* args) override; @@ -53,10 +57,12 @@ private: float _lineHeight; glm::vec3 _textColor; float _textAlpha; - glm::vec3 _backgroundColor; - float _backgroundAlpha; bool _unlit; + std::shared_ptr _material { std::make_shared() }; + glm::vec3 _backgroundColor { NAN }; + float _backgroundAlpha { NAN }; + float _leftMargin; float _rightMargin; float _topMargin; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5c52cb2708..524c631631 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -723,7 +723,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * * @property {Vec3} naturalPosition=0,0,0 - The center of the entity's unscaled mesh model if it has one, otherwise * {@link Vec3(0)|Vec3.ZERO}. Read-only. - * @property {Vec3} naturalDimensions - The dimensions of the entity's unscaled mesh model if it has one, otherwise + * @property {Vec3} naturalDimensions - The dimensions of the entity's unscaled mesh model or image if it has one, otherwise * {@link Vec3(0)|Vec3.ONE}. Read-only. * * @property {Vec3} velocity=0,0,0 - The linear velocity of the entity in m/s with respect to world coordinates. diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 36beb9f0d3..90c521215e 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -57,6 +57,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership connect(nodeList.data(), &NodeList::canRezTmpCertifiedChanged, this, &EntityScriptingInterface::canRezTmpCertifiedChanged); connect(nodeList.data(), &NodeList::canWriteAssetsChanged, this, &EntityScriptingInterface::canWriteAssetsChanged); connect(nodeList.data(), &NodeList::canGetAndSetPrivateUserDataChanged, this, &EntityScriptingInterface::canGetAndSetPrivateUserDataChanged); + connect(nodeList.data(), &NodeList::canRezAvatarEntitiesChanged, this, &EntityScriptingInterface::canRezAvatarEntitiesChanged); auto& packetReceiver = nodeList->getPacketReceiver(); packetReceiver.registerListener(PacketType::EntityScriptCallMethod, @@ -114,6 +115,11 @@ bool EntityScriptingInterface::canGetAndSetPrivateUserData() { return nodeList->getThisNodeCanGetAndSetPrivateUserData(); } +bool EntityScriptingInterface::canRezAvatarEntities() { + auto nodeList = DependencyManager::get(); + return nodeList->getThisNodeCanRezAvatarEntities(); +} + void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) { if (_entityTree) { disconnect(_entityTree.get(), &EntityTree::addingEntityPointer, this, &EntityScriptingInterface::onAddingEntity); @@ -481,6 +487,15 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr _activityTracking.addedEntityCount++; + auto nodeList = DependencyManager::get(); + + if (entityHostType == entity::HostType::AVATAR && !nodeList->getThisNodeCanRezAvatarEntities()) { + qCDebug(entities) << "Ignoring addEntity() because don't have canRezAvatarEntities permission on domain"; + // Only need to intercept methods that may add an avatar entity because avatar entities are removed from the tree when + // the user doesn't have canRezAvatarEntities permission. + return QUuid(); + } + EntityItemProperties propertiesWithSimID = properties; propertiesWithSimID.setEntityHostType(entityHostType); if (entityHostType == entity::HostType::AVATAR) { @@ -493,7 +508,6 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr } // the created time will be set in EntityTree::addEntity by recordCreationTime() - auto nodeList = DependencyManager::get(); auto sessionID = nodeList->getSessionUUID(); propertiesWithSimID.setLastEditedBy(sessionID); @@ -1002,7 +1016,7 @@ void EntityScriptingInterface::deleteEntity(const QUuid& id) { for (auto entity : entitiesToDeleteImmediately) { if (entity->isMyAvatarEntity()) { - getEntityPacketSender()->getMyAvatar()->clearAvatarEntity(entity->getID(), false); + getEntityPacketSender()->getMyAvatar()->clearAvatarEntityInternal(entity->getID()); } } } diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 14d853fbaf..4647e26335 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -289,6 +289,14 @@ public slots: * privateUserData property of entities, otherwise false. */ Q_INVOKABLE bool canGetAndSetPrivateUserData(); + + /**jsdoc + * Checks whether or not the script can rez avatar entities. + * @function Entities.canRezAvatarEntities + * @returns {boolean} true if the domain server will allow the script to rez avatar entities, + * otherwise false. + */ + Q_INVOKABLE bool canRezAvatarEntities(); /**jsdoc *

How an entity is hosted and sent to others for display.

@@ -2255,11 +2263,20 @@ signals: /**jsdoc * Triggered when your ability to get and set private user data changes. * @function Entities.canGetAndSetPrivateUserDataChanged - * @param {boolean} canGetAndSetPrivateUserData - true if the script change the privateUserData + * @param {boolean} canGetAndSetPrivateUserData - true if the script can change the privateUserData * property of an entity, false if it can't. * @returns {Signal} */ void canGetAndSetPrivateUserDataChanged(bool canGetAndSetPrivateUserData); + + /**jsdoc + * Triggered when your ability to use avatar entities is changed. + * @function Entities.canRezAvatarEntitiesChanged + * @param {boolean} canRezAvatarEntities - true if the script can change edit avatar entities, + * false if it can't. + * @returns {Signal} + */ + void canRezAvatarEntitiesChanged(bool canRezAvatarEntities); /**jsdoc diff --git a/libraries/entities/src/ImageEntityItem.cpp b/libraries/entities/src/ImageEntityItem.cpp index afa6d9ae69..41cdde7676 100644 --- a/libraries/entities/src/ImageEntityItem.cpp +++ b/libraries/entities/src/ImageEntityItem.cpp @@ -34,6 +34,7 @@ EntityItemProperties ImageEntityItem::getProperties(const EntityPropertyFlags& d COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); withReadLock([&] { _pulseProperties.getProperties(properties); + properties.setNaturalDimensions(_naturalDimensions); }); COPY_ENTITY_PROPERTY_TO_PROPERTIES(imageURL, getImageURL); @@ -217,4 +218,10 @@ PulsePropertyGroup ImageEntityItem::getPulseProperties() const { return resultWithReadLock([&] { return _pulseProperties; }); -} \ No newline at end of file +} + +void ImageEntityItem::setNaturalDimension(const glm::vec3& naturalDimensions) const { + withWriteLock([&] { + _naturalDimensions = naturalDimensions; + }); +} diff --git a/libraries/entities/src/ImageEntityItem.h b/libraries/entities/src/ImageEntityItem.h index 4f7aac0c13..0cc4eae05a 100644 --- a/libraries/entities/src/ImageEntityItem.h +++ b/libraries/entities/src/ImageEntityItem.h @@ -63,6 +63,8 @@ public: PulsePropertyGroup getPulseProperties() const; + void setNaturalDimension(const glm::vec3& naturalDimensions) const; + protected: glm::u8vec3 _color; float _alpha; @@ -72,6 +74,8 @@ protected: bool _emissive { false }; bool _keepAspectRatio { true }; QRect _subImage; + + mutable glm::vec3 _naturalDimensions; }; #endif // hifi_ImageEntityItem_h diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 4349eefd2d..ceebf20123 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -339,7 +339,7 @@ bool ShapeEntityItem::findDetailedParabolaIntersection(const glm::vec3& origin, bool ShapeEntityItem::getRotateForPicking() const { auto shape = getShape(); - return getBillboardMode() != BillboardMode::NONE && (_shape < entity::Shape::Cube || _shape > entity::Shape::Icosahedron); + return getBillboardMode() != BillboardMode::NONE && (shape < entity::Shape::Cube || shape > entity::Shape::Icosahedron); } void ShapeEntityItem::debugDump() const { diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index d4e2833784..0916316ec1 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -1863,7 +1863,7 @@ void GLTFSerializer::setHFMMaterial(HFMMaterial& hfmMat, const GLTFMaterial& mat template bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int count, - QVector& outarray, int accessorType) { + QVector& outarray, int accessorType, bool normalized) { QDataStream blobstream(bin); blobstream.setByteOrder(QDataStream::LittleEndian); @@ -1899,12 +1899,22 @@ bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int c blobstream.setDevice(nullptr); return false; } + + float scale = 1.0f; // Normalized output values should always be floats. + if (normalized) { + scale = (float)(std::numeric_limits::max)(); + } + for (int i = 0; i < count; ++i) { for (int j = 0; j < bufferCount; ++j) { if (!blobstream.atEnd()) { T value; blobstream >> value; - outarray.push_back(value); + if (normalized) { + outarray.push_back(std::max((float)value / scale, -1.0f)); + } else { + outarray.push_back(value); + } } else { blobstream.setDevice(nullptr); return false; @@ -1917,24 +1927,24 @@ bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int c } template bool GLTFSerializer::addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, - QVector& outarray, int accessorType, int componentType) { + QVector& outarray, int accessorType, int componentType, bool normalized) { switch (componentType) { case GLTFAccessorComponentType::BYTE: {} case GLTFAccessorComponentType::UNSIGNED_BYTE: { - return readArray(bin, byteOffset, count, outarray, accessorType); + return readArray(bin, byteOffset, count, outarray, accessorType, normalized); } case GLTFAccessorComponentType::SHORT: { - return readArray(bin, byteOffset, count, outarray, accessorType); + return readArray(bin, byteOffset, count, outarray, accessorType, normalized); } case GLTFAccessorComponentType::UNSIGNED_INT: { - return readArray(bin, byteOffset, count, outarray, accessorType); + return readArray(bin, byteOffset, count, outarray, accessorType, normalized); } case GLTFAccessorComponentType::UNSIGNED_SHORT: { - return readArray(bin, byteOffset, count, outarray, accessorType); + return readArray(bin, byteOffset, count, outarray, accessorType, normalized); } case GLTFAccessorComponentType::FLOAT: { - return readArray(bin, byteOffset, count, outarray, accessorType); + return readArray(bin, byteOffset, count, outarray, accessorType, normalized); } } return false; @@ -1951,11 +1961,11 @@ bool GLTFSerializer::addArrayFromAccessor(GLTFAccessor& accessor, QVector& ou int accBoffset = accessor.defined["byteOffset"] ? accessor.byteOffset : 0; success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, accessor.count, outarray, accessor.type, - accessor.componentType); + accessor.componentType, accessor.normalized); } else { for (int i = 0; i < accessor.count; ++i) { T value; - memset(&value, 0, sizeof(T)); // Make sure the dummy array is initalised to zero. + memset(&value, 0, sizeof(T)); // Make sure the dummy array is initialized to zero. outarray.push_back(value); } } @@ -1971,7 +1981,7 @@ bool GLTFSerializer::addArrayFromAccessor(GLTFAccessor& accessor, QVector& ou success = addArrayOfType(sparseIndicesBuffer.blob, sparseIndicesBufferview.byteOffset + accSIBoffset, accessor.sparse.count, out_sparse_indices_array, GLTFAccessorType::SCALAR, - accessor.sparse.indices.componentType); + accessor.sparse.indices.componentType, false); if (success) { QVector out_sparse_values_array; @@ -1981,7 +1991,8 @@ bool GLTFSerializer::addArrayFromAccessor(GLTFAccessor& accessor, QVector& ou int accSVBoffset = accessor.sparse.values.defined["byteOffset"] ? accessor.sparse.values.byteOffset : 0; success = addArrayOfType(sparseValuesBuffer.blob, sparseValuesBufferview.byteOffset + accSVBoffset, - accessor.sparse.count, out_sparse_values_array, accessor.type, accessor.componentType); + accessor.sparse.count, out_sparse_values_array, accessor.type, accessor.componentType, + accessor.normalized); if (success) { for (int i = 0; i < accessor.sparse.count; ++i) { diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index 79524ea7ff..040ccb413b 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -527,7 +527,7 @@ struct GLTFAccessor { int componentType; //required int count; //required int type; //required - bool normalized{ false }; + bool normalized { false }; QVector max; QVector min; GLTFAccessorSparse sparse; @@ -832,11 +832,11 @@ private: template bool readArray(const hifi::ByteArray& bin, int byteOffset, int count, - QVector& outarray, int accessorType); + QVector& outarray, int accessorType, bool normalized); template bool addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, - QVector& outarray, int accessorType, int componentType); + QVector& outarray, int accessorType, int componentType, bool normalized); template bool addArrayFromAccessor(GLTFAccessor& accessor, QVector& outarray); diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp index 8126988294..dd30727523 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp @@ -840,10 +840,7 @@ void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) { if (_input._colorAttribute != newColor) { _input._colorAttribute = newColor; glVertexAttrib4fv(gpu::Stream::COLOR, &_input._colorAttribute.r); - // Color has been changed and is not white. To prevent colors from bleeding - // between different objects, we need to set the _hadColorAttribute flag - // as if a previous render call had potential colors - _input._hadColorAttribute = (newColor != glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); + _input._hasColorAttribute = true; } (void)CHECK_GL_ERROR(); } diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h index 0c8676493b..2947649ce7 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h @@ -348,36 +348,37 @@ protected: virtual void updateInput() = 0; struct InputStageState { - bool _invalidFormat{ true }; - bool _lastUpdateStereoState{ false }; - bool _hadColorAttribute{ true }; - FormatReference _format{ GPU_REFERENCE_INIT_VALUE }; + bool _invalidFormat { true }; + bool _lastUpdateStereoState { false }; + bool _hasColorAttribute { false }; + bool _hadColorAttribute { false }; + FormatReference _format { GPU_REFERENCE_INIT_VALUE }; std::string _formatKey; typedef std::bitset ActivationCache; - ActivationCache _attributeActivation{ 0 }; + ActivationCache _attributeActivation { 0 }; typedef std::bitset BuffersState; - BuffersState _invalidBuffers{ 0 }; - BuffersState _attribBindingBuffers{ 0 }; + BuffersState _invalidBuffers { 0 }; + BuffersState _attribBindingBuffers { 0 }; - std::array _buffers{}; - std::array _bufferOffsets{}; - std::array _bufferStrides{}; - std::array _bufferVBOs{}; + std::array _buffers; + std::array _bufferOffsets; + std::array _bufferStrides; + std::array _bufferVBOs; - glm::vec4 _colorAttribute{ 0.0f }; + glm::vec4 _colorAttribute { 1.0f }; - BufferReference _indexBuffer{}; - Offset _indexBufferOffset{ 0 }; - Type _indexBufferType{ UINT32 }; + BufferReference _indexBuffer; + Offset _indexBufferOffset { 0 }; + Type _indexBufferType { UINT32 }; - BufferReference _indirectBuffer{}; - Offset _indirectBufferOffset{ 0 }; - Offset _indirectBufferStride{ 0 }; + BufferReference _indirectBuffer; + Offset _indirectBufferOffset { 0 }; + Offset _indirectBufferStride { 0 }; - GLuint _defaultVAO{ 0 }; + GLuint _defaultVAO { 0 }; } _input; virtual void initTransform() = 0; diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp index 85e6ba5382..4efd5f9941 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp @@ -103,6 +103,9 @@ void GLBackend::resetInputStage() { reset(_input._format); _input._formatKey.clear(); _input._invalidFormat = false; + _input._hasColorAttribute = false; + _input._hadColorAttribute = false; + _input._colorAttribute = vec4(1.0f); _input._attributeActivation.reset(); for (uint32_t i = 0; i < _input._buffers.size(); i++) { @@ -159,15 +162,15 @@ void GLBackend::updateInput() { _input._invalidFormat |= (isStereoNow != _input._lastUpdateStereoState); #endif _input._lastUpdateStereoState = isStereoNow; - + + bool hasColorAttribute = _input._hasColorAttribute; + if (_input._invalidFormat) { InputStageState::ActivationCache newActivation; // Assign the vertex format required auto format = acquire(_input._format); if (format) { - bool hasColorAttribute{ false }; - _input._attribBindingBuffers.reset(); const auto& attributes = format->getAttributes(); @@ -186,12 +189,12 @@ void GLBackend::updateInput() { uint8_t locationCount = attrib._element.getLocationCount(); GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; - GLuint offset = (GLuint)attrib._offset;; + GLuint offset = (GLuint)attrib._offset; GLboolean isNormalized = attrib._element.isNormalized(); GLenum perLocationSize = attrib._element.getLocationSize(); - hasColorAttribute = hasColorAttribute || (slot == Stream::COLOR); + hasColorAttribute |= slot == Stream::COLOR; for (GLuint locNum = 0; locNum < locationCount; ++locNum) { GLuint attriNum = (GLuint)(slot + locNum); @@ -224,14 +227,11 @@ void GLBackend::updateInput() { #endif } - if (_input._hadColorAttribute && !hasColorAttribute) { - // The previous input stage had a color attribute but this one doesn't so reset - // color to pure white. - const auto white = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); - glVertexAttrib4fv(Stream::COLOR, &white.r); - _input._colorAttribute = white; + if (!hasColorAttribute && _input._hadColorAttribute) { + // The previous input stage had a color attribute but this one doesn't, so reset the color to pure white. + _input._colorAttribute = glm::vec4(1.0f); + glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r); } - _input._hadColorAttribute = hasColorAttribute; } // Manage Activation what was and what is expected now @@ -253,6 +253,9 @@ void GLBackend::updateInput() { _stats._ISNumFormatChanges++; } + _input._hadColorAttribute = hasColorAttribute; + _input._hasColorAttribute = false; + if (_input._invalidBuffers.any()) { auto vbo = _input._bufferVBOs.data(); auto offset = _input._bufferOffsets.data(); @@ -276,4 +279,3 @@ void GLBackend::updateInput() { (void)CHECK_GL_ERROR(); } } - diff --git a/libraries/gpu-gl/CMakeLists.txt b/libraries/gpu-gl/CMakeLists.txt index 01f097df87..3c569f7a44 100644 --- a/libraries/gpu-gl/CMakeLists.txt +++ b/libraries/gpu-gl/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME gpu-gl) setup_hifi_library(Concurrent) link_hifi_libraries(shared gl gpu gpu-gl-common shaders) -if (UNIX) +if (UNIX AND NOT VIRCADIA_THREAD_DEBUGGING) target_link_libraries(${TARGET_NAME} pthread) endif(UNIX) GroupSources("src") diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp index 2b985c122e..188b4a1084 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp @@ -33,6 +33,8 @@ void GL41Backend::updateInput() { #endif _input._lastUpdateStereoState = isStereoNow; + bool hasColorAttribute = _input._hasColorAttribute; + if (_input._invalidFormat || _input._invalidBuffers.any()) { auto format = acquire(_input._format); @@ -71,8 +73,6 @@ void GL41Backend::updateInput() { // now we need to bind the buffers and assign the attrib pointers if (format) { - bool hasColorAttribute{ false }; - const auto& buffers = _input._buffers; const auto& offsets = _input._bufferOffsets; const auto& strides = _input._bufferStrides; @@ -110,7 +110,7 @@ void GL41Backend::updateInput() { uintptr_t pointer = (uintptr_t)(attrib._offset + offsets[bufferNum]); GLboolean isNormalized = attrib._element.isNormalized(); - hasColorAttribute = hasColorAttribute || (slot == Stream::COLOR); + hasColorAttribute |= slot == Stream::COLOR; for (size_t locNum = 0; locNum < locationCount; ++locNum) { if (attrib._element.isInteger()) { @@ -132,17 +132,16 @@ void GL41Backend::updateInput() { } } - if (_input._hadColorAttribute && !hasColorAttribute) { - // The previous input stage had a color attribute but this one doesn't so reset - // color to pure white. - const auto white = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); - glVertexAttrib4fv(Stream::COLOR, &white.r); - _input._colorAttribute = white; + if (!hasColorAttribute && _input._hadColorAttribute) { + // The previous input stage had a color attribute but this one doesn't, so reset the color to pure white. + _input._colorAttribute = glm::vec4(1.0f); + glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r); } - _input._hadColorAttribute = hasColorAttribute; } // everything format related should be in sync now _input._invalidFormat = false; } -} + _input._hadColorAttribute = hasColorAttribute; + _input._hasColorAttribute = false; +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp index 5285e62d3e..8add4a9296 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp @@ -35,14 +35,14 @@ void GL45Backend::updateInput() { #endif _input._lastUpdateStereoState = isStereoNow; + bool hasColorAttribute = _input._hasColorAttribute; + if (_input._invalidFormat) { InputStageState::ActivationCache newActivation; // Assign the vertex format required auto format = acquire(_input._format); if (format) { - bool hasColorAttribute{ false }; - _input._attribBindingBuffers.reset(); const auto& attributes = format->getAttributes(); @@ -61,12 +61,12 @@ void GL45Backend::updateInput() { uint8_t locationCount = attrib._element.getLocationCount(); GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; - GLuint offset = (GLuint)attrib._offset;; + GLuint offset = (GLuint)attrib._offset; GLboolean isNormalized = attrib._element.isNormalized(); GLenum perLocationSize = attrib._element.getLocationSize(); - hasColorAttribute = hasColorAttribute || (slot == Stream::COLOR); + hasColorAttribute |= slot == Stream::COLOR; for (GLuint locNum = 0; locNum < locationCount; ++locNum) { GLuint attriNum = (GLuint)(slot + locNum); @@ -99,14 +99,11 @@ void GL45Backend::updateInput() { #endif } - if (_input._hadColorAttribute && !hasColorAttribute) { - // The previous input stage had a color attribute but this one doesn't so reset - // color to pure white. - const auto white = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f); - glVertexAttrib4fv(Stream::COLOR, &white.r); - _input._colorAttribute = white; + if (!hasColorAttribute && _input._hadColorAttribute) { + // The previous input stage had a color attribute but this one doesn't, so reset the color to pure white. + _input._colorAttribute = glm::vec4(1.0f); + glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r); } - _input._hadColorAttribute = hasColorAttribute; } // Manage Activation what was and what is expected now @@ -128,6 +125,9 @@ void GL45Backend::updateInput() { _stats._ISNumFormatChanges++; } + _input._hadColorAttribute = hasColorAttribute; + _input._hasColorAttribute = false; + if (_input._invalidBuffers.any()) { auto vbo = _input._bufferVBOs.data(); auto offset = _input._bufferOffsets.data(); diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 54c7d49421..a525cda1ab 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -579,11 +579,11 @@ public: ExternalUpdates getUpdates() const; // Serialize a texture into a KTX file - static ktx::KTXUniquePointer serialize(const Texture& texture); + static ktx::KTXUniquePointer serialize(const Texture& texture, const glm::ivec2& originalSize); - static TexturePointer build(const ktx::KTXDescriptor& descriptor); - static TexturePointer unserialize(const std::string& ktxFile); - static TexturePointer unserialize(const cache::FilePointer& cacheEntry, const std::string& source = std::string()); + static std::pair build(const ktx::KTXDescriptor& descriptor); + static std::pair unserialize(const std::string& ktxFile); + static std::pair unserialize(const cache::FilePointer& cacheEntry, const std::string& source = std::string()); static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat); diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index a5cea3e60e..c4b674a917 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -13,6 +13,7 @@ #include "Texture.h" #include +#include #include @@ -30,15 +31,16 @@ struct GPUKTXPayload { using Version = uint8; static const std::string KEY; - static const Version CURRENT_VERSION { 1 }; + static const Version CURRENT_VERSION { 2 }; static const size_t PADDING { 2 }; - static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + PADDING }; - static_assert(GPUKTXPayload::SIZE == 36, "Packing size may differ between platforms"); + static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + sizeof(glm::ivec2) + PADDING }; + static_assert(GPUKTXPayload::SIZE == 44, "Packing size may differ between platforms"); static_assert(GPUKTXPayload::SIZE % 4 == 0, "GPUKTXPayload is not 4 bytes aligned"); Sampler::Desc _samplerDesc; Texture::Usage _usage; TextureUsageType _usageType; + glm::ivec2 _originalSize { 0, 0 }; Byte* serialize(Byte* data) const { *(Version*)data = CURRENT_VERSION; @@ -56,26 +58,22 @@ struct GPUKTXPayload { memcpy(data, &_usageType, sizeof(TextureUsageType)); data += sizeof(TextureUsageType); + memcpy(data, glm::value_ptr(_originalSize), sizeof(glm::ivec2)); + data += sizeof(glm::ivec2); + return data + PADDING; } bool unserialize(const Byte* data, size_t size) { - if (size != SIZE) { + Version version = *(const Version*)data; + data += sizeof(Version); + + if (version > CURRENT_VERSION) { + // If we try to load a version that we don't know how to parse, + // it will render incorrectly return false; } - Version version = *(const Version*)data; - if (version != CURRENT_VERSION) { - glm::vec4 borderColor(1.0f); - if (memcmp(&borderColor, data, sizeof(glm::vec4)) == 0) { - memcpy(this, data, sizeof(GPUKTXPayload)); - return true; - } else { - return false; - } - } - data += sizeof(Version); - memcpy(&_samplerDesc, data, sizeof(Sampler::Desc)); data += sizeof(Sampler::Desc); @@ -87,6 +85,13 @@ struct GPUKTXPayload { data += sizeof(uint32); memcpy(&_usageType, data, sizeof(TextureUsageType)); + data += sizeof(TextureUsageType); + + if (version >= 2) { + memcpy(&_originalSize, data, sizeof(glm::ivec2)); + data += sizeof(glm::ivec2); + } + return true; } @@ -382,7 +387,7 @@ void Texture::setKtxBacking(const cache::FilePointer& cacheEntry) { } -ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { +ktx::KTXUniquePointer Texture::serialize(const Texture& texture, const glm::ivec2& originalSize) { ktx::Header header; // From texture format to ktx format description @@ -459,6 +464,7 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { gpuKeyval._samplerDesc = texture.getSampler().getDesc(); gpuKeyval._usage = texture.getUsage(); gpuKeyval._usageType = texture.getUsageType(); + gpuKeyval._originalSize = originalSize; Byte keyvalPayload[GPUKTXPayload::SIZE]; gpuKeyval.serialize(keyvalPayload); @@ -514,19 +520,19 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { return ktxBuffer; } -TexturePointer Texture::build(const ktx::KTXDescriptor& descriptor) { +std::pair Texture::build(const ktx::KTXDescriptor& descriptor) { Format mipFormat = Format::COLOR_BGRA_32; Format texelFormat = Format::COLOR_SRGBA_32; const auto& header = descriptor.header; if (!Texture::evalTextureFormat(header, mipFormat, texelFormat)) { - return nullptr; + return { nullptr, { 0, 0 } }; } // Find Texture Type based on dimensions Type type = TEX_1D; if (header.pixelWidth == 0) { - return nullptr; + return { nullptr, { 0, 0 } }; } else if (header.pixelHeight == 0) { type = TEX_1D; } else if (header.pixelDepth == 0) { @@ -569,39 +575,39 @@ TexturePointer Texture::build(const ktx::KTXDescriptor& descriptor) { texture->overrideIrradiance(std::make_shared(irradianceKtxKeyValue._irradianceSH)); } - return texture; + return { texture, gpuktxKeyValue._originalSize }; } -TexturePointer Texture::unserialize(const cache::FilePointer& cacheEntry, const std::string& source) { +std::pair Texture::unserialize(const cache::FilePointer& cacheEntry, const std::string& source) { std::unique_ptr ktxPointer = ktx::KTX::create(std::make_shared(cacheEntry->getFilepath().c_str())); if (!ktxPointer) { - return nullptr; + return { nullptr, { 0, 0 } }; } - auto texture = build(ktxPointer->toDescriptor()); - if (texture) { - texture->setKtxBacking(cacheEntry); - if (texture->source().empty()) { - texture->setSource(source); + auto textureAndSize = build(ktxPointer->toDescriptor()); + if (textureAndSize.first) { + textureAndSize.first->setKtxBacking(cacheEntry); + if (textureAndSize.first->source().empty()) { + textureAndSize.first->setSource(source); } } - return texture; + return { textureAndSize.first, textureAndSize.second }; } -TexturePointer Texture::unserialize(const std::string& ktxfile) { +std::pair Texture::unserialize(const std::string& ktxfile) { std::unique_ptr ktxPointer = ktx::KTX::create(std::make_shared(ktxfile.c_str())); if (!ktxPointer) { - return nullptr; + return { nullptr, { 0, 0 } }; } - auto texture = build(ktxPointer->toDescriptor()); - if (texture) { - texture->setKtxBacking(ktxfile); - texture->setSource(ktxfile); + auto textureAndSize = build(ktxPointer->toDescriptor()); + if (textureAndSize.first) { + textureAndSize.first->setKtxBacking(ktxfile); + textureAndSize.first->setSource(ktxfile); } - return texture; + return { textureAndSize.first, textureAndSize.second }; } bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) { diff --git a/libraries/image/src/image/TextureProcessing.cpp b/libraries/image/src/image/TextureProcessing.cpp index 53991ae431..1b45fc1bae 100644 --- a/libraries/image/src/image/TextureProcessing.cpp +++ b/libraries/image/src/image/TextureProcessing.cpp @@ -338,9 +338,9 @@ void mapToRedChannel(Image& image, ColorChannel sourceChannel) { } } -gpu::TexturePointer processImage(std::shared_ptr content, const std::string& filename, ColorChannel sourceChannel, - int maxNumPixels, TextureUsage::Type textureType, - bool compress, BackendTarget target, const std::atomic& abortProcessing) { +std::pair processImage(std::shared_ptr content, const std::string& filename, ColorChannel sourceChannel, + int maxNumPixels, TextureUsage::Type textureType, + bool compress, BackendTarget target, const std::atomic& abortProcessing) { Image image = processRawImageData(*content.get(), filename); // Texture content can take up a lot of memory. Here we release our ownership of that content @@ -354,7 +354,7 @@ gpu::TexturePointer processImage(std::shared_ptr content, const std:: if (imageWidth == 0 || imageHeight == 0 || image.getFormat() == Image::Format_Invalid) { QString reason(image.getFormat() == Image::Format_Invalid ? "(Invalid Format)" : "(Size is invalid)"); qCWarning(imagelogging) << "Failed to load:" << qPrintable(reason); - return nullptr; + return { nullptr, { imageWidth, imageHeight } }; } // Validate the image is less than _maxNumPixels, and downscale if necessary @@ -378,7 +378,7 @@ gpu::TexturePointer processImage(std::shared_ptr content, const std:: auto loader = TextureUsage::getTextureLoaderForType(textureType); auto texture = loader(std::move(image), filename, compress, target, abortProcessing); - return texture; + return { texture, { imageWidth, imageHeight } }; } Image processSourceImage(Image&& srcImage, bool cubemap, BackendTarget target) { diff --git a/libraries/image/src/image/TextureProcessing.h b/libraries/image/src/image/TextureProcessing.h index 326ec700a9..decb940fda 100644 --- a/libraries/image/src/image/TextureProcessing.h +++ b/libraries/image/src/image/TextureProcessing.h @@ -121,9 +121,9 @@ gpu::TexturePointer processCubeTextureColorFromImage(Image&& srcImage, const std const QStringList getSupportedFormats(); -gpu::TexturePointer processImage(std::shared_ptr content, const std::string& url, ColorChannel sourceChannel, - int maxNumPixels, TextureUsage::Type textureType, - bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing = false); +std::pair processImage(std::shared_ptr content, const std::string& url, ColorChannel sourceChannel, + int maxNumPixels, TextureUsage::Type textureType, + bool compress, gpu::BackendTarget target, const std::atomic& abortProcessing = false); void convertToTextureWithMips(gpu::Texture* texture, Image&& image, gpu::BackendTarget target, const std::atomic& abortProcessing = false, int face = -1); void convertToTexture(gpu::Texture* texture, Image&& image, gpu::BackendTarget target, const std::atomic& abortProcessing = false, int face = -1, int mipLevel = 0); diff --git a/libraries/material-networking/src/material-networking/KTXCache.cpp b/libraries/material-networking/src/material-networking/KTXCache.cpp index edb09addb8..5a5a94dcc9 100644 --- a/libraries/material-networking/src/material-networking/KTXCache.cpp +++ b/libraries/material-networking/src/material-networking/KTXCache.cpp @@ -19,7 +19,7 @@ using FilePointer = cache::FilePointer; // Whenever a change is made to the serialized format for the KTX cache that isn't backward compatible, // this value should be incremented. This will force the KTX cache to be wiped -const int KTXCache::CURRENT_VERSION = 0x01; +const int KTXCache::CURRENT_VERSION = 0x02; const int KTXCache::INVALID_VERSION = 0x00; const char* KTXCache::SETTING_VERSION_NAME = "hifi.ktx.cache_version"; diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 64c3abb0d8..e4e36cad48 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -266,23 +266,24 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs return ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash()(extra)).staticCast(); } -gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { - std::weak_ptr weakPointer; +std::pair TextureCache::getTextureByHash(const std::string& hash) { + std::pair weakPointer; { std::unique_lock lock(_texturesByHashesMutex); weakPointer = _texturesByHashes[hash]; } - return weakPointer.lock(); + return { weakPointer.first.lock(), weakPointer.second }; } -gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) { - gpu::TexturePointer result; +std::pair TextureCache::cacheTextureByHash(const std::string& hash, const std::pair& textureAndSize) { + std::pair result; { std::unique_lock lock(_texturesByHashesMutex); - result = _texturesByHashes[hash].lock(); - if (!result) { - _texturesByHashes[hash] = texture; - result = texture; + auto& value = _texturesByHashes[hash]; + result = { value.first.lock(), value.second }; + if (!result.first) { + _texturesByHashes[hash] = textureAndSize; + result = textureAndSize; } } return result; @@ -616,7 +617,7 @@ void NetworkTexture::makeLocalRequest() { ktxDescriptor = std::make_shared(ktxFile->toDescriptor()); } - gpu::TexturePointer texture; + std::pair textureAndSize; if (ktxDescriptor) { std::string hash; // Create bare ktx in memory @@ -634,18 +635,18 @@ void NetworkTexture::makeLocalRequest() { } auto textureCache = DependencyManager::get(); - texture = textureCache->getTextureByHash(hash); - if (!texture) { - texture = gpu::Texture::build(*ktxDescriptor); - if (texture) { - texture->setKtxBacking(path.toStdString()); - texture->setSource(path.toStdString()); - texture = textureCache->cacheTextureByHash(hash, texture); + textureAndSize = textureCache->getTextureByHash(hash); + if (!textureAndSize.first) { + textureAndSize = gpu::Texture::build(*ktxDescriptor); + if (textureAndSize.first) { + textureAndSize.first->setKtxBacking(path.toStdString()); + textureAndSize.first->setSource(path.toStdString()); + textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize); } } } - if (!texture) { + if (!textureAndSize.first) { qCDebug(networking).noquote() << "Failed load local KTX from" << path; QMetaObject::invokeMethod(this, "setImage", Q_ARG(gpu::TexturePointer, nullptr), @@ -655,11 +656,11 @@ void NetworkTexture::makeLocalRequest() { } _ktxResourceState = PENDING_MIP_REQUEST; - _lowestKnownPopulatedMip = texture->minAvailableMipLevel(); + _lowestKnownPopulatedMip = textureAndSize.first->minAvailableMipLevel(); QMetaObject::invokeMethod(this, "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, texture->getWidth()), - Q_ARG(int, texture->getHeight())); + Q_ARG(gpu::TexturePointer, textureAndSize.first), + Q_ARG(int, textureAndSize.second.x), + Q_ARG(int, textureAndSize.second.y)); } @@ -968,22 +969,22 @@ void NetworkTexture::handleFinishedInitialLoad() { auto textureCache = DependencyManager::get(); - gpu::TexturePointer texture = textureCache->getTextureByHash(hash); + std::pair textureAndSize = textureCache->getTextureByHash(hash); - if (!texture) { + if (!textureAndSize.first) { auto ktxFile = textureCache->_ktxCache->getFile(hash); if (ktxFile) { - texture = gpu::Texture::unserialize(ktxFile); - if (texture) { - texture = textureCache->cacheTextureByHash(hash, texture); - if (texture->source().empty()) { - texture->setSource(url.toString().toStdString()); + textureAndSize = gpu::Texture::unserialize(ktxFile); + if (textureAndSize.first) { + textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize); + if (textureAndSize.first->source().empty()) { + textureAndSize.first->setSource(url.toString().toStdString()); } } } } - if (!texture) { + if (!textureAndSize.first) { auto memKtx = ktx::KTX::createBare(*header, keyValues); if (!memKtx) { qWarning() << " Ktx could not be created, bailing"; @@ -1010,9 +1011,9 @@ void NetworkTexture::handleFinishedInitialLoad() { auto newKtxDescriptor = memKtx->toDescriptor(); - texture = gpu::Texture::build(newKtxDescriptor); - texture->setKtxBacking(file); - texture->setSource(filename); + textureAndSize = gpu::Texture::build(newKtxDescriptor); + textureAndSize.first->setKtxBacking(file); + textureAndSize.first->setSource(filename); auto& images = originalKtxDescriptor->images; size_t imageSizeRemaining = ktxHighMipData.size(); @@ -1025,7 +1026,7 @@ void NetworkTexture::handleFinishedInitialLoad() { break; } ktxData -= image._imageSize; - texture->assignStoredMip(static_cast(level), image._imageSize, ktxData); + textureAndSize.first->assignStoredMip(static_cast(level), image._imageSize, ktxData); ktxData -= ktx::IMAGE_SIZE_WIDTH; imageSizeRemaining -= (image._imageSize + ktx::IMAGE_SIZE_WIDTH); } @@ -1033,13 +1034,13 @@ void NetworkTexture::handleFinishedInitialLoad() { // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will // be the winner - texture = textureCache->cacheTextureByHash(filename, texture); + textureAndSize = textureCache->cacheTextureByHash(filename, textureAndSize); } QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, texture->getWidth()), - Q_ARG(int, texture->getHeight())); + Q_ARG(gpu::TexturePointer, textureAndSize.first), + Q_ARG(int, textureAndSize.second.x), + Q_ARG(int, textureAndSize.second.y)); QMetaObject::invokeMethod(resource.data(), "startRequestForNextMipLevel"); }); @@ -1229,15 +1230,15 @@ void ImageReader::read() { auto textureCache = DependencyManager::get(); if (textureCache) { // If we already have a live texture with the same hash, use it - auto texture = textureCache->getTextureByHash(hash); + auto textureAndSize = textureCache->getTextureByHash(hash); // If there is no live texture, check if there's an existing KTX file - if (!texture) { + if (!textureAndSize.first) { auto ktxFile = textureCache->_ktxCache->getFile(hash); if (ktxFile) { - texture = gpu::Texture::unserialize(ktxFile, _url.toString().toStdString()); - if (texture) { - texture = textureCache->cacheTextureByHash(hash, texture); + textureAndSize = gpu::Texture::unserialize(ktxFile, _url.toString().toStdString()); + if (textureAndSize.first) { + textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize); } else { qCWarning(materialnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating..."; } @@ -1246,17 +1247,17 @@ void ImageReader::read() { // If we found the texture either because it's in use or via KTX deserialization, // set the image and return immediately. - if (texture) { + if (textureAndSize.first) { QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, texture->getWidth()), - Q_ARG(int, texture->getHeight())); + Q_ARG(gpu::TexturePointer, textureAndSize.first), + Q_ARG(int, textureAndSize.second.x), + Q_ARG(int, textureAndSize.second.y)); return; } } // Proccess new texture - gpu::TexturePointer texture; + std::pair textureAndSize; { PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); @@ -1269,23 +1270,23 @@ void ImageReader::read() { constexpr bool shouldCompress = false; #endif auto target = getBackendTarget(); - texture = image::processImage(std::move(buffer), _url.toString().toStdString(), _sourceChannel, _maxNumPixels, networkTexture->getTextureType(), shouldCompress, target); + textureAndSize = image::processImage(std::move(buffer), _url.toString().toStdString(), _sourceChannel, _maxNumPixels, networkTexture->getTextureType(), shouldCompress, target); - if (!texture) { + if (!textureAndSize.first) { QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), + Q_ARG(gpu::TexturePointer, textureAndSize.first), Q_ARG(int, 0), Q_ARG(int, 0)); return; } - texture->setSourceHash(hash); - texture->setFallbackTexture(networkTexture->getFallbackTexture()); + textureAndSize.first->setSourceHash(hash); + textureAndSize.first->setFallbackTexture(networkTexture->getFallbackTexture()); } // Save the image into a KTXFile - if (texture && textureCache) { - auto memKtx = gpu::Texture::serialize(*texture); + if (textureAndSize.first && textureCache) { + auto memKtx = gpu::Texture::serialize(*textureAndSize.first, textureAndSize.second); // Move the texture into a memory mapped file if (memKtx) { @@ -1294,20 +1295,20 @@ void ImageReader::read() { auto& ktxCache = textureCache->_ktxCache; auto file = ktxCache->writeFile(data, KTXCache::Metadata(hash, length)); if (file) { - texture->setKtxBacking(file); + textureAndSize.first->setKtxBacking(file); } } // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will // be the winner - texture = textureCache->cacheTextureByHash(hash, texture); + textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize); } QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, texture->getWidth()), - Q_ARG(int, texture->getHeight())); + Q_ARG(gpu::TexturePointer, textureAndSize.first), + Q_ARG(int, textureAndSize.second.x), + Q_ARG(int, textureAndSize.second.y)); } NetworkTexturePointer TextureCache::getResourceTexture(const QUrl& resourceTextureUrl) { diff --git a/libraries/material-networking/src/material-networking/TextureCache.h b/libraries/material-networking/src/material-networking/TextureCache.h index 754263566b..aab458e184 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.h +++ b/libraries/material-networking/src/material-networking/TextureCache.h @@ -183,8 +183,8 @@ public: const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, image::ColorChannel sourceChannel = image::ColorChannel::NONE); - gpu::TexturePointer getTextureByHash(const std::string& hash); - gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); + std::pair getTextureByHash(const std::string& hash); + std::pair cacheTextureByHash(const std::string& hash, const std::pair& textureAndSize); NetworkTexturePointer getResourceTexture(const QUrl& resourceTextureUrl); const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); @@ -226,7 +226,7 @@ private: std::shared_ptr _ktxCache { std::make_shared(KTX_DIRNAME, KTX_EXT) }; // Map from image hashes to texture weak pointers - std::unordered_map> _texturesByHashes; + std::unordered_map, glm::ivec2>> _texturesByHashes; std::mutex _texturesByHashesMutex; gpu::TexturePointer _permutationNormalTexture; diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 76587118f1..f0fa1365c4 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -415,6 +415,8 @@ void AddressManager::handleLookupString(const QString& lookupString, bool fromSu QString sanitizedString = lookupString.trimmed(); if (!sanitizedString.isEmpty()) { + resetConfirmConnectWithoutAvatarEntities(); + // make this a valid hifi URL and handle it off to handleUrl handleUrl(sanitizedString, fromSuggestions ? Suggestions : UserInput); } @@ -874,6 +876,11 @@ bool AddressManager::setDomainInfo(const QUrl& domainURL, LookupTrigger trigger) return emitHostChanged; } +void AddressManager::goToEntry(LookupTrigger trigger) { + resetConfirmConnectWithoutAvatarEntities(); + handleUrl(DEFAULT_VIRCADIA_ADDRESS, trigger); +} + void AddressManager::goToUser(const QString& username, bool shouldMatchOrientation) { QString formattedUsername = QUrl::toPercentEncoding(username); @@ -890,6 +897,11 @@ void AddressManager::goToUser(const QString& username, bool shouldMatchOrientati QByteArray(), nullptr, requestParams); } +void AddressManager::goToLastAddress() { + resetConfirmConnectWithoutAvatarEntities(); + handleUrl(_lastVisitedURL, LookupTrigger::AttemptedRefresh); +} + bool AddressManager::canGoBack() const { return (_backStack.size() > 0); } @@ -1024,3 +1036,10 @@ QString AddressManager::getPlaceName() const { } return QString(); } + +void AddressManager::resetConfirmConnectWithoutAvatarEntities() { + DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); + if (!domainHandler.isConnected()) { + domainHandler.resetConfirmConnectWithoutAvatarEntities(); + } +} diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index daca6d3392..f491e59b97 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -301,9 +301,7 @@ public slots: * @param {location.LookupTrigger} trigger=StartupFromSettings - The reason for the function call. Helps ensure that user's * location history is correctly maintained. */ - void goToEntry(LookupTrigger trigger = LookupTrigger::StartupFromSettings) { - handleUrl(DEFAULT_VIRCADIA_ADDRESS, trigger); - } + void goToEntry(LookupTrigger trigger = LookupTrigger::StartupFromSettings); /**jsdoc * Takes you to the specified user's location. @@ -318,7 +316,7 @@ public slots: * Takes you to the last address tried. This will be the last URL tried from location.handleLookupString. * @function location.goToLastAddress */ - void goToLastAddress() { handleUrl(_lastVisitedURL, LookupTrigger::AttemptedRefresh); } + void goToLastAddress(); /**jsdoc * Checks if going back to the previous location is possible. @@ -527,6 +525,8 @@ private: void addCurrentAddressToHistory(LookupTrigger trigger); + void resetConfirmConnectWithoutAvatarEntities(); + QUrl _domainURL; QUrl _lastVisitedURL; diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 6a47d74864..5a1d8fb4a0 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -126,6 +126,8 @@ void DomainHandler::hardReset(QString reason) { emit resetting(); softReset(reason); + _haveAskedConnectWithoutAvatarEntities = false; + _canConnectWithoutAvatarEntities = false; _isInErrorState = false; emit redirectErrorStateChanged(_isInErrorState); @@ -364,10 +366,14 @@ void DomainHandler::setIsConnected(bool isConnected) { _lastDomainConnectionError = -1; emit connectedToDomain(_domainURL); + // FIXME: Reinstate the requestDomainSettings() call here in version 2021.2.0 instead of having it in + // NodeList::processDomainServerList(). + /* if (_domainURL.scheme() == URL_SCHEME_HIFI && !_domainURL.host().isEmpty()) { // we've connected to new domain - time to ask it for global settings requestDomainSettings(); } + */ } else { emit disconnectedFromDomain(); @@ -375,6 +381,24 @@ void DomainHandler::setIsConnected(bool isConnected) { } } +void DomainHandler::setCanConnectWithoutAvatarEntities(bool canConnect) { + _canConnectWithoutAvatarEntities = canConnect; + _haveAskedConnectWithoutAvatarEntities = true; +} + +bool DomainHandler::canConnectWithoutAvatarEntities() { + if (!_canConnectWithoutAvatarEntities && !_haveAskedConnectWithoutAvatarEntities) { + if (_isConnected) { + // Already connected so don't ask. (Permission removed from user while in the domain.) + setCanConnectWithoutAvatarEntities(true); + } else { + // Ask whether to connect to the domain. + emit confirmConnectWithoutAvatarEntities(); + } + } + return _canConnectWithoutAvatarEntities; +} + void DomainHandler::connectedToServerless(std::map namedPaths) { _namedPaths = namedPaths; setIsConnected(true); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 56d32d8609..fd188ecec0 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -98,6 +98,7 @@ public: Node::LocalID getLocalID() const { return _localID; } void setLocalID(Node::LocalID localID) { _localID = localID; } + QString getScheme() const { return _domainURL.scheme(); } QString getHostname() const { return _domainURL.host(); } QUrl getErrorDomainURL(){ return _errorDomainURL; } @@ -133,6 +134,9 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); + void setCanConnectWithoutAvatarEntities(bool canConnect); + bool canConnectWithoutAvatarEntities(); + bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; } bool getInterstitialModeEnabled() const; void setInterstitialModeEnabled(bool enableInterstitialMode); @@ -159,6 +163,10 @@ public: bool checkInPacketTimeout(); void clearPendingCheckins() { _checkInPacketsSinceLastReply = 0; } + void resetConfirmConnectWithoutAvatarEntities() { + _haveAskedConnectWithoutAvatarEntities = false; + } + /**jsdoc *

The reasons that you may be refused connection to a domain are defined by numeric values:

* @@ -252,6 +260,7 @@ signals: void completedSocketDiscovery(); void resetting(); + void confirmConnectWithoutAvatarEntities(); void connectedToDomain(QUrl domainURL); void disconnectedFromDomain(); @@ -287,6 +296,8 @@ private: HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; bool _isConnected { false }; + bool _haveAskedConnectWithoutAvatarEntities { false }; + bool _canConnectWithoutAvatarEntities { false }; bool _isInErrorState { false }; QJsonObject _settingsObject; QString _pendingPath; diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 3da5b53dcb..653611ae8c 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -198,6 +198,10 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { newPermissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData)) { emit canGetAndSetPrivateUserDataChanged(_permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData)); } + if (originalPermissions.can(NodePermissions::Permission::canRezAvatarEntities) != + newPermissions.can(NodePermissions::Permission::canRezAvatarEntities)) { + emit canRezAvatarEntitiesChanged(_permissions.can(NodePermissions::Permission::canRezAvatarEntities)); + } } void LimitedNodeList::setSocketLocalPort(quint16 socketLocalPort) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 59d18fe047..189f3e1b08 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -133,6 +133,7 @@ public: bool getThisNodeCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } bool getThisNodeCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } bool getThisNodeCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); } + bool getThisNodeCanRezAvatarEntities() const { return _permissions.can(NodePermissions::Permission::canRezAvatarEntities); } quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); } Q_INVOKABLE void setSocketLocalPort(quint16 socketLocalPort); @@ -390,6 +391,7 @@ signals: void canKickChanged(bool canKick); void canReplaceContentChanged(bool canReplaceContent); void canGetAndSetPrivateUserDataChanged(bool canGetAndSetPrivateUserData); + void canRezAvatarEntitiesChanged(bool canRezAvatarEntities); protected slots: void connectedForLocalSocketTest(); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 07c599913b..7435ebaaf8 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -84,6 +84,7 @@ public: bool getCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } bool getCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } bool getCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); } + bool getCanRezAvatarEntities() const { return _permissions.can(NodePermissions::Permission::canRezAvatarEntities); } using NodesIgnoredPair = std::pair, bool>; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index a523a7ff36..a975302699 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -42,6 +42,7 @@ #include "udt/PacketHeaders.h" #include "SharedUtil.h" #include +#include using namespace std::chrono; @@ -94,6 +95,12 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) // send a ping punch immediately connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer); + // FIXME: Can remove this temporary work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) + // Adjust our canRezAvatarEntities permissions on older domains that do not have this setting. + // DomainServerList and DomainSettings packets can come in either order so need to adjust with both occurrences. + auto nodeList = DependencyManager::get(); + connect(&_domainHandler, &DomainHandler::settingsReceived, this, &NodeList::adjustCanRezAvatarEntitiesPerSettings); + auto accountManager = DependencyManager::get(); // assume that we may need to send a new DS check in anytime a new keypair is generated @@ -726,6 +733,11 @@ void NodeList::processDomainServerList(QSharedPointer message) // pull the permissions/right/privileges for this node out of the stream NodePermissions newPermissions; packetStream >> newPermissions; + // FIXME: Can remove this temporary work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) + // Adjust our canRezAvatarEntities permissions on older domains that do not have this setting. + // DomainServerList and DomainSettings packets can come in either order so need to adjust with both occurrences. + bool adjustedPermissions = adjustCanRezAvatarEntitiesPermissions(_domainHandler.getSettingsObject(), newPermissions, false); + // Is packet authentication enabled? bool isAuthenticated; packetStream >> isAuthenticated; @@ -781,7 +793,7 @@ void NodeList::processDomainServerList(QSharedPointer message) DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); if (_domainHandler.isConnected() && _domainHandler.getUUID() != domainUUID) { - // Recieved packet from different domain. + // Received packet from different domain. qWarning() << "IGNORING DomainList packet from" << domainUUID << "while connected to" << _domainHandler.getUUID() << ": sent " << pingLagTime << " msec ago."; qWarning(networking) << "DomainList request lag (interface->ds): " << domainServerRequestLag << "msec"; @@ -809,6 +821,23 @@ void NodeList::processDomainServerList(QSharedPointer message) setSessionLocalID(newLocalID); setSessionUUID(newUUID); + // FIXME: Remove this call to requestDomainSettings() and reinstate the one in DomainHandler::setIsConnected(), in version + // 2021.2.0. (New protocol version implies a domain server upgrade.) + if (!_domainHandler.isConnected() + && _domainHandler.getScheme() == URL_SCHEME_HIFI && !_domainHandler.getHostname().isEmpty()) { + // We're about to connect but we need the domain settings (in particular, the node permissions) in order to adjust the + // canRezAvatarEntities permission above before using the permissions in determining whether or not to connect without + // avatar entities rezzing below. + _domainHandler.requestDomainSettings(); + } + + // Don't continue login to the domain if have avatar entities and don't have permissions to rez them, unless user has OKed + // continuing login. + if (!newPermissions.can(NodePermissions::Permission::canRezAvatarEntities) + && (!adjustedPermissions || !_domainHandler.canConnectWithoutAvatarEntities())) { + return; + } + // if this was the first domain-server list from this domain, we've now connected if (!_domainHandler.isConnected()) { _domainHandler.setLocalID(domainLocalID); @@ -1263,17 +1292,19 @@ float NodeList::getInjectorGain() { return _injectorGain; } -void NodeList::kickNodeBySessionID(const QUuid& nodeID) { +void NodeList::kickNodeBySessionID(const QUuid& nodeID, unsigned int banFlags) { // 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) if (!nodeID.isNull() && getSessionUUID() != nodeID ) { if (getThisNodeCanKick()) { // setup the packet - auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID, true); + auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID + sizeof(int), true); // write the node ID to the packet kickPacket->write(nodeID.toRfc4122()); + // write the ban parameters to the packet + kickPacket->writePrimitive(banFlags); qCDebug(networking) << "Sending packet to kick node" << uuidStringWithoutCurlyBraces(nodeID); @@ -1368,3 +1399,35 @@ void NodeList::setRequestsDomainListData(bool isRequesting) { void NodeList::startThread() { moveToNewNamedThread(this, "NodeList Thread", QThread::TimeCriticalPriority); } + + +// FIXME: Can remove this work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) +bool NodeList::adjustCanRezAvatarEntitiesPermissions(const QJsonObject& domainSettingsObject, + NodePermissions& permissions, bool notify) { + + if (domainSettingsObject.isEmpty()) { + // Don't have enough information to adjust yet. + return false; // Failed to adjust. + } + + const double CANREZAVATARENTITIES_INTRODUCED_VERSION = 2.5; + auto version = domainSettingsObject.value("version"); + if (version.isUndefined() || version.isDouble() && version.toDouble() < CANREZAVATARENTITIES_INTRODUCED_VERSION) { + // On domains without the canRezAvatarEntities permission available, set it to the same as canConnectToDomain. + if (permissions.can(NodePermissions::Permission::canConnectToDomain)) { + if (!permissions.can(NodePermissions::Permission::canRezAvatarEntities)) { + permissions.set(NodePermissions::Permission::canRezAvatarEntities); + if (notify) { + emit canRezAvatarEntitiesChanged(permissions.can(NodePermissions::Permission::canRezAvatarEntities)); + } + } + } + } + + return true; // Successfully adjusted. +} + +// FIXME: Can remove this work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) +void NodeList::adjustCanRezAvatarEntitiesPerSettings(const QJsonObject& domainSettingsObject) { + adjustCanRezAvatarEntitiesPermissions(domainSettingsObject, _permissions, true); +} diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 4954c53c84..59b3815fba 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -86,7 +86,7 @@ public: void setInjectorGain(float gain); float getInjectorGain(); - void kickNodeBySessionID(const QUuid& nodeID); + void kickNodeBySessionID(const QUuid& nodeID, unsigned int banFlags); void muteNodeBySessionID(const QUuid& nodeID); void requestUsernameFromSessionID(const QUuid& nodeID); bool getRequestsDomainListData() { return _requestsDomainListData; } @@ -123,6 +123,11 @@ public slots: void processUsernameFromIDReply(QSharedPointer message); + // FIXME: Can remove these work-arounds in version 2021.2.0. (New protocol version implies a domain server upgrade.) + bool adjustCanRezAvatarEntitiesPermissions(const QJsonObject& domainSettingsObject, NodePermissions& permissions, + bool notify); + void adjustCanRezAvatarEntitiesPerSettings(const QJsonObject& domainSettingsObject); + #if (PR_BUILD || DEV_BUILD) void toggleSendNewerDSConnectVersion(bool shouldSendNewerVersion) { _shouldSendNewerVersion = shouldSendNewerVersion; } #endif diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index e0de649c05..d96cdfcf06 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -57,6 +57,7 @@ NodePermissions::NodePermissions(QMap perms) { permissions = NodePermissions::Permissions(); permissions |= perms["id_can_connect"].toBool() ? Permission::canConnectToDomain : Permission::none; + permissions |= perms["id_can_rez_avatar_entities"].toBool() ? Permission::canRezAvatarEntities : Permission::none; permissions |= perms["id_can_adjust_locks"].toBool() ? Permission::canAdjustLocks : Permission::none; permissions |= perms["id_can_rez"].toBool() ? Permission::canRezPermanentEntities : Permission::none; permissions |= perms["id_can_rez_tmp"].toBool() ? Permission::canRezTemporaryEntities : Permission::none; @@ -67,7 +68,8 @@ NodePermissions::NodePermissions(QMap perms) { Permission::canConnectPastMaxCapacity : Permission::none; permissions |= perms["id_can_kick"].toBool() ? Permission::canKick : Permission::none; permissions |= perms["id_can_replace_content"].toBool() ? Permission::canReplaceDomainContent : Permission::none; - permissions |= perms["id_can_get_and_set_private_user_data"].toBool() ? Permission::canGetAndSetPrivateUserData : Permission::none; + permissions |= perms["id_can_get_and_set_private_user_data"].toBool() ? + Permission::canGetAndSetPrivateUserData : Permission::none; } QVariant NodePermissions::toVariant(QHash groupRanks) { @@ -86,6 +88,7 @@ QVariant NodePermissions::toVariant(QHash groupRanks) { } } values["id_can_connect"] = can(Permission::canConnectToDomain); + values["id_can_rez_avatar_entities"] = can(Permission::canRezAvatarEntities); values["id_can_adjust_locks"] = can(Permission::canAdjustLocks); values["id_can_rez"] = can(Permission::canRezPermanentEntities); values["id_can_rez_tmp"] = can(Permission::canRezTemporaryEntities); @@ -141,6 +144,9 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) { if (perms.can(NodePermissions::Permission::canConnectToDomain)) { debug << " connect"; } + if (perms.can(NodePermissions::Permission::canRezAvatarEntities)) { + debug << " rez-avatar-entities"; + } if (perms.can(NodePermissions::Permission::canAdjustLocks)) { debug << " locks"; } @@ -174,6 +180,7 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) { debug.nospace() << "]"; return debug.nospace(); } + QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms) { if (perms) { return operator<<(debug, *perms.get()); diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 82c008feef..e3bfc69d07 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -80,7 +80,8 @@ public: canReplaceDomainContent = 128, canRezPermanentCertifiedEntities = 256, canRezTemporaryCertifiedEntities = 512, - canGetAndSetPrivateUserData = 1024 + canGetAndSetPrivateUserData = 1024, + canRezAvatarEntities = 2048 }; Q_DECLARE_FLAGS(Permissions, Permission) Permissions permissions; diff --git a/libraries/networking/src/PacketReceiver.h b/libraries/networking/src/PacketReceiver.h index 071638cda9..4d2f11360e 100644 --- a/libraries/networking/src/PacketReceiver.h +++ b/libraries/networking/src/PacketReceiver.h @@ -85,9 +85,9 @@ private: class UnsourcedListenerReference : public ListenerReference { public: inline UnsourcedListenerReference(T* target, void (T::*slot)(QSharedPointer)); - virtual bool invokeDirectly(const QSharedPointer& receivedMessagePointer, const QSharedPointer& sourceNode); - virtual bool isSourced() const { return false; } - virtual QObject* getObject() const { return _target; } + virtual bool invokeDirectly(const QSharedPointer& receivedMessagePointer, const QSharedPointer& sourceNode) override; + virtual bool isSourced() const override { return false; } + virtual QObject* getObject() const override { return _target; } private: QPointer _target; @@ -98,9 +98,9 @@ private: class SourcedListenerReference : public ListenerReference { public: inline SourcedListenerReference(T* target, void (T::*slot)(QSharedPointer, QSharedPointer)); - virtual bool invokeDirectly(const QSharedPointer& receivedMessagePointer, const QSharedPointer& sourceNode); - virtual bool isSourced() const { return true; } - virtual QObject* getObject() const { return _target; } + virtual bool invokeDirectly(const QSharedPointer& receivedMessagePointer, const QSharedPointer& sourceNode) override; + virtual bool isSourced() const override { return true; } + virtual QObject* getObject() const override { return _target; } private: QPointer _target; diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index 1cda1235e9..ea01a8446c 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -4,6 +4,7 @@ // // Created by Ryan Huffman on 6/06/16. // Copyright 2016 High Fidelity, Inc. +// Copyright 2021 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 777a3d3f87..2eaf771ae2 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -696,10 +696,29 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel return; } - // rotate into motor-frame + const bool motorHasRotation = !(motor.rotation == btQuaternion::getIdentity()); btVector3 axis = motor.rotation.getAxis(); btScalar angle = motor.rotation.getAngle(); - btVector3 velocity = worldVelocity.rotate(axis, -angle); + + // Rotate a vector from motor frame to world frame + auto rotateToWorldFrame = [&axis, &angle, &motorHasRotation](const btVector3 vectorInMotorFrame) { + if (motorHasRotation) { + return vectorInMotorFrame.rotate(axis, angle); + } else { + return vectorInMotorFrame; + } + }; + + // Rotate a vector from world frame to motor frame + auto rotateToMotorFrame = [&axis, &angle, &motorHasRotation](const btVector3 vectorInWorldFrame) { + if (motorHasRotation) { + return vectorInWorldFrame.rotate(axis, -angle); + } else { + return vectorInWorldFrame; + } + }; + + btVector3 velocity = rotateToMotorFrame(worldVelocity); int32_t collisionMask = computeCollisionMask(); if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS || @@ -712,15 +731,15 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel velocity += tau * (motor.velocity - velocity); // rotate back into world-frame - velocity = velocity.rotate(axis, angle); - _targetVelocity += (tau * motor.velocity).rotate(axis, angle); + velocity = rotateToWorldFrame(velocity); + _targetVelocity += rotateToWorldFrame(tau * motor.velocity); // store the velocity and weight velocities.push_back(velocity); weights.push_back(tau); } else { // compute local UP - btVector3 up = _currentUp.rotate(axis, -angle); + btVector3 up = rotateToMotorFrame(_currentUp); btVector3 motorVelocity = motor.velocity; // save these non-adjusted components for later @@ -729,7 +748,7 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel if (_stepHeight > _minStepHeight && !_steppingUp) { // there is a step --> compute velocity direction to go over step - btVector3 motorVelocityWF = motorVelocity.rotate(axis, angle); + btVector3 motorVelocityWF = rotateToWorldFrame(motorVelocity); if (motorVelocityWF.dot(_stepNormal) < 0.0f) { // the motor pushes against step _steppingUp = true; @@ -764,8 +783,8 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel } // add components back together and rotate into world-frame - velocity = (hVelocity + vVelocity).rotate(axis, angle); - _targetVelocity += maxTau * (hTargetVelocity + vTargetVelocity).rotate(axis, angle); + velocity = rotateToWorldFrame(hVelocity + vVelocity); + _targetVelocity += maxTau * rotateToWorldFrame(hTargetVelocity + vTargetVelocity); // store velocity and weights velocities.push_back(velocity); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 325e228120..af33899617 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -821,14 +821,12 @@ void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape) { void GeometryCache::renderShape(gpu::Batch& batch, Shape shape, const glm::vec4& color) { batch.setInputFormat(getSolidStreamFormat()); - // Color must be set after input format batch._glColor4f(color.r, color.g, color.b, color.a); _shapes[shape].draw(batch); } void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape, const glm::vec4& color) { batch.setInputFormat(getWireStreamFormat()); - // Color must be set after input format batch._glColor4f(color.r, color.g, color.b, color.a); _shapes[shape].drawWire(batch); } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index b5cff41bf8..1a3a898582 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -92,7 +92,7 @@ private: bool _cauterized { false }; bool _cullWithParent { false }; QVector _renderWithZones; - BillboardMode _billboardMode; + BillboardMode _billboardMode { BillboardMode::NONE }; uint64_t _created; Transform _localTransform; diff --git a/libraries/script-engine/src/ScriptsModel.h b/libraries/script-engine/src/ScriptsModel.h index 0412bbf0fe..0721600efc 100644 --- a/libraries/script-engine/src/ScriptsModel.h +++ b/libraries/script-engine/src/ScriptsModel.h @@ -35,6 +35,7 @@ public: void setParent(TreeNodeFolder* parent) { _parent = parent; } TreeNodeType getType() { return _type; } const QString& getName() { return _name; }; + virtual ~TreeNodeBase() = default; private: TreeNodeFolder* _parent; diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 9beb52f20a..883c728c2f 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 2016-07-11. // Copyright 2016 High Fidelity, Inc. +// Copyright 2021 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -51,15 +52,14 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { return DependencyManager::get()->getAvatarGain(nodeID); } -void UsersScriptingInterface::kick(const QUuid& nodeID) { - +void UsersScriptingInterface::kick(const QUuid& nodeID, unsigned int banFlags) { if (_kickConfirmationOperator) { bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); if (getCanKick() && !waitingForKickResponse) { - _kickConfirmationOperator(nodeID); + _kickConfirmationOperator(nodeID, banFlags); } } else { - DependencyManager::get()->kickNodeBySessionID(nodeID); + DependencyManager::get()->kickNodeBySessionID(nodeID, banFlags); } } diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 3b0666481a..79c0f4b61d 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 2016-07-11. // Copyright 2016 High Fidelity, Inc. +// Copyright 2021 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -16,6 +17,7 @@ #include #include +#include /**jsdoc * The Users API provides features to regulate your interaction with other users. @@ -31,6 +33,10 @@ * false. Read-only. * @property {boolean} requestsDomainListData - true if the client requests extra data from the mixers (such as * positional data of an avatar they've ignored). Read-only. + * @property {BanFlags} NO_BAN - Do not ban user. Read-only. + * @property {BanFlags} BAN_BY_USERNAME - Ban user by username. Read-only. + * @property {BanFlags} BAN_BY_FINGERPRINT - Ban user by fingerprint. Read-only. + * @property {BanFlags} BAN_BY_IP - Ban user by IP address. Read-only. */ class UsersScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -39,9 +45,14 @@ class UsersScriptingInterface : public QObject, public Dependency { Q_PROPERTY(bool canKick READ getCanKick) Q_PROPERTY(bool requestsDomainListData READ getRequestsDomainListData WRITE setRequestsDomainListData) + Q_PROPERTY(unsigned int NO_BAN READ getNoBan CONSTANT) + Q_PROPERTY(unsigned int BAN_BY_USERNAME READ getBanByUsername CONSTANT) + Q_PROPERTY(unsigned int BAN_BY_FINGERPRINT READ getBanByFingerprint CONSTANT) + Q_PROPERTY(unsigned int BAN_BY_IP READ getBanByIP CONSTANT) + public: UsersScriptingInterface(); - void setKickConfirmationOperator(std::function kickConfirmationOperator) { + void setKickConfirmationOperator(std::function kickConfirmationOperator) { _kickConfirmationOperator = kickConfirmationOperator; } @@ -111,13 +122,14 @@ public slots: float getAvatarGain(const QUuid& nodeID); /**jsdoc - * Kicks and bans a user. This removes them from the server and prevents them from returning. The ban is by user name if - * available, or machine fingerprint otherwise. + * Kicks and bans a user. This removes them from the server and prevents them from returning. The ban is by user name (if + * available) and by machine fingerprint. The ban functionality can be controlled with flags. *

This function only works if you're an administrator of the domain you're in.

* @function Users.kick * @param {Uuid} sessionID - The session ID of the user to kick and ban. + * @param {BanFlags} - Preferred ban flags. Bans a user by username (if available) and machine fingerprint by default. */ - void kick(const QUuid& nodeID); + void kick(const QUuid& nodeID, unsigned int banFlags = ModerationFlags::getDefaultBanFlags()); /**jsdoc * Mutes a user's microphone for everyone. The mute is not permanent: the user can unmute themselves. @@ -237,7 +249,12 @@ private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); - std::function _kickConfirmationOperator; + static constexpr unsigned int getNoBan() { return ModerationFlags::BanFlags::NO_BAN; }; + static constexpr unsigned int getBanByUsername() { return ModerationFlags::BanFlags::BAN_BY_USERNAME; }; + static constexpr unsigned int getBanByFingerprint() { return ModerationFlags::BanFlags::BAN_BY_FINGERPRINT; }; + static constexpr unsigned int getBanByIP() { return ModerationFlags::BanFlags::BAN_BY_IP; }; + + std::function _kickConfirmationOperator; ReadWriteLockable _kickResponseLock; bool _waitingForKickResponse { false }; diff --git a/libraries/shared/src/ModerationFlags.h b/libraries/shared/src/ModerationFlags.h new file mode 100644 index 0000000000..a8390873d7 --- /dev/null +++ b/libraries/shared/src/ModerationFlags.h @@ -0,0 +1,45 @@ +// +// ModerationFlags.h +// libraries/shared/src +// +// Created by Kalila L. on Mar 11 2021. +// Copyright 2021 Vircadia contributors. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef vircadia_ModerationFlags_h +#define vircadia_ModerationFlags_h + +class ModerationFlags { +public: + + /**jsdoc + *

A set of flags for moderation ban actions. The value is constructed by using the | (bitwise OR) operator on the + * individual flag values.

+ *
+ * + * + * + * + * + * + * + * + * + *
Flag NameValueDescription
NO_BAN0Don't ban user when kicking. This does not currently have an effect.
BAN_BY_USERNAME1Ban the person by their username.
BAN_BY_FINGERPRINT2Ban the person by their machine fingerprint.
BAN_BY_IP4Ban the person by their IP address.
+ * @typedef {number} BanFlags + */ + enum BanFlags + { + NO_BAN = 0, + BAN_BY_USERNAME = 1, + BAN_BY_FINGERPRINT = 2, + BAN_BY_IP = 4 + }; + + static constexpr unsigned int getDefaultBanFlags() { return (BanFlags::BAN_BY_USERNAME | BanFlags::BAN_BY_FINGERPRINT); }; +}; + +#endif // vircadia_ModerationFlags_h diff --git a/scripts/system/create/assets/images/processing.gif b/scripts/system/create/assets/images/processing.gif new file mode 100644 index 0000000000..94d1486b0a Binary files /dev/null and b/scripts/system/create/assets/images/processing.gif differ diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 9bd0147002..ed9763875d 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -35,7 +35,8 @@ Script.include([ "../libraries/gridTool.js", "entityList/entityList.js", "entitySelectionTool/entitySelectionTool.js", - "audioFeedback/audioFeedback.js" + "audioFeedback/audioFeedback.js", + "modules/brokenURLReport.js" ]); var CreateWindow = Script.require('./modules/createWindow.js'); diff --git a/scripts/system/create/entityList/entityList.js b/scripts/system/create/entityList/entityList.js index 5119d7d3da..02cd9a14ca 100644 --- a/scripts/system/create/entityList/entityList.js +++ b/scripts/system/create/entityList/entityList.js @@ -413,12 +413,14 @@ EntityListTool = function(shouldUseEditTabletApp) { alignGridToSelection(); } else if (data.type === 'alignGridToAvatar') { alignGridToAvatar(); + } else if (data.type === 'brokenURLReport') { + brokenURLReport(selectionManager.selections); } else if (data.type === 'toggleGridVisibility') { toggleGridVisibility(); } else if (data.type === 'toggleSnapToGrid') { - that.toggleSnapToGrid(); + that.toggleSnapToGrid(); } - + }; webView.webEventReceived.connect(onWebEventReceived); diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html index e054ca121b..75b172e201 100644 --- a/scripts/system/create/entityList/html/entityList.html +++ b/scripts/system/create/entityList/html/entityList.html @@ -308,7 +308,14 @@ - + + + diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index d21e65d47d..e3526296d9 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -273,6 +273,7 @@ let elEntityTable, elSnapToGridActivatorCaption, elAlignGridToSelection, elAlignGridToAvatar, + elBrokenURLReport, elFilterTypeMultiselectBox, elFilterTypeText, elFilterTypeOptions, @@ -359,7 +360,8 @@ function loaded() { elSnapToGridActivator = document.getElementById("snapToGridActivator"); elSnapToGridActivatorCaption = document.getElementById("snapToGridActivatorCaption"); elAlignGridToSelection = document.getElementById("alignGridToSelection"); - elAlignGridToAvatar = document.getElementById("alignGridToAvatar"); + elAlignGridToAvatar = document.getElementById("alignGridToAvatar"); + elBrokenURLReport = document.getElementById("brokenURLReport"); elFilterTypeMultiselectBox = document.getElementById("filter-type-multiselect-box"); elFilterTypeText = document.getElementById("filter-type-text"); elFilterTypeOptions = document.getElementById("filter-type-options"); @@ -597,7 +599,11 @@ function loaded() { elAlignGridToAvatar.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: "alignGridToAvatar" })); closeAllEntityListMenu(); - }; + }; + elBrokenURLReport.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ type: "brokenURLReport" })); + closeAllEntityListMenu(); + }; elToggleSpaceMode.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: "toggleSpaceMode" })); }; diff --git a/scripts/system/create/modules/brokenURLReport.html b/scripts/system/create/modules/brokenURLReport.html new file mode 100644 index 0000000000..6720bf29a2 --- /dev/null +++ b/scripts/system/create/modules/brokenURLReport.html @@ -0,0 +1,102 @@ + + + + + + + + +
+
+

+
+
+ Testing in progress... +

+
+ +
+
+ + + diff --git a/scripts/system/create/modules/brokenURLReport.js b/scripts/system/create/modules/brokenURLReport.js new file mode 100644 index 0000000000..c3a6eba8cd --- /dev/null +++ b/scripts/system/create/modules/brokenURLReport.js @@ -0,0 +1,393 @@ +// +// brokenURLReport.js +// +// Created by Alezia Kurdis on February 22, 2021. +// Copyright 2021 Vircadia contributors. +// +// This script reports broken URLs to the Create Application. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var brokenURLReportHttpRequest; +var brokenURLReportUrlList = []; +var brokenURLReportInvalideUrlList = []; +var brokenURLReportProcessedUrlNo = 0; +var brokenURLReportUrlEntry; +var brokenURLReportMessageBox; +var brokenURLReportOverlayWebWindow; +var BROKEN_URL_REPORT_YES_BUTTON = 0x4000; +var BROKEN_URL_REPORT_NO_BUTTON = 0x10000; +var MAX_URL_BEFORE_WARNING_FOR_LONG_PROCESS = 20; + +function brokenURLReportRequestUrlValidityCheck(no) { + brokenURLReportHttpRequest = new XMLHttpRequest(); + brokenURLReportHttpRequest.requestComplete.connect(brokenURLReportGetResponseStatus); + brokenURLReportHttpRequest.open("GET", brokenURLReportUrlList[no].url); + brokenURLReportHttpRequest.send(); +} + +function brokenURLReportGetResponseStatus() { + if (brokenURLReportHttpRequest.status === 0 || brokenURLReportHttpRequest.status > 299) { + if (brokenURLReportHttpRequest.status === 0) { + brokenURLReportUrlList[brokenURLReportProcessedUrlNo].validity = "0 - URL not well-formed"; + } else { + brokenURLReportUrlList[brokenURLReportProcessedUrlNo].validity = brokenURLReportHttpRequest.status + " - " + brokenURLReportHttpRequest.statusText; + } + brokenURLReportInvalideUrlList.push(brokenURLReportUrlList[brokenURLReportProcessedUrlNo]); + } + brokenURLReportHttpRequest.requestComplete.disconnect(brokenURLReportGetResponseStatus); + brokenURLReportHttpRequest = null; + brokenURLReportProcessedUrlNo = brokenURLReportProcessedUrlNo + 1; + if (brokenURLReportProcessedUrlNo === brokenURLReportUrlList.length) { + brokenURLReportGenerateFormatedReport(brokenURLReportInvalideUrlList); + brokenURLReportUrlList = []; + brokenURLReportInvalideUrlList = []; + brokenURLReportProcessedUrlNo = 0; + } else { + brokenURLReportRequestUrlValidityCheck(brokenURLReportProcessedUrlNo); + } +} + +function brokenURLReportGenerateFormatedReport(brokenURLReportInvalideUrlList) { + var brokenURLReportContent = ""; + if (brokenURLReportInvalideUrlList.length === 0) { + brokenURLReportContent = "

Broken URL Report

" + brokenURLReportUrlList.length + " URL tested.
NO ISSUES HAVE BEEN FOUND.




"; + brokenURLReportContent += "

This report ignores Asset Server URLs (atp://), local drive paths, and any string not starting with 'http'.
"; + Script.setTimeout(function () { + brokenURLReportOverlayWebWindow.emitScriptEvent(brokenURLReportContent); + }, 3000); + return; + } + brokenURLReportContent = "

Broken URL Report

\n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + for (var i = 0; i < brokenURLReportInvalideUrlList.length; i++ ){ + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + brokenURLReportContent += " \n"; + } + brokenURLReportContent += "
 EntityBroken Url
NoTypeName & ID PropertyStatusCurrent URL
" + (i + 1) + "" + brokenURLReportInvalideUrlList[i].type + "" + brokenURLReportInvalideUrlList[i].name + "
" + brokenURLReportInvalideUrlList[i].id + "
" + brokenURLReportInvalideUrlList[i].urlType + "" + brokenURLReportInvalideUrlList[i].validity + "" + brokenURLReportInvalideUrlList[i].url + "
\n"; + brokenURLReportContent += "

" + brokenURLReportUrlList.length + " URL tested.

"; + brokenURLReportContent += "
This report ignores Asset Server URLs (atp://), local drive paths, and any string not starting with 'http'.
"; + + Script.setTimeout(function () { + brokenURLReportOverlayWebWindow.emitScriptEvent(brokenURLReportContent); + }, 3000); +} + +function brokenURLReportGetUrlTypeColor(urlType) { + var color = "#FFFFFF"; + switch (urlType) { + case "script": + color = "#00FF00"; + break; + case "serverScripts": + color = "#FF00FF"; + break; + case "imageURL": + color = "#00FFFF"; + break; + case "materialURL": + color = "#FF6600"; + break; + case "modelURL": + color = "#FFFF00"; + break; + case "compoundShapeURL": + color = "#6666FF"; + break; + case "animation.url": + color = "#6699FF"; + break; + case "textures": + color = "#FF0066"; + break; + case "xTextureURL": + color = "#0000FF"; + break; + case "yTextureURL": + color = "#009966"; + break; + case "zTextureURL": + color = "#993366"; + break; + case "font": + color = "#FFFFFF"; + break; + case "sourceUrl": + color = "#BBFF00"; + break; + case "scriptURL": + color = "#FFBBBB"; + break; + case "filterURL": + color = "#BBBBFF"; + break; + case "skybox.url": + color = "#BBFFFF"; + break; + case "ambientLight.ambientURL": + color = "#FF3300"; + } + return color; +} + +function brokenURLReport(entityIDs) { + if (entityIDs.length === 0) { + audioFeedback.rejection(); + Window.alert("You have nothing selected."); + return; + } else { + var properties; + for (var i = 0; i < entityIDs.length; i++ ){ + properties = Entities.getEntityProperties(entityIDs[i]); + if (properties.script.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "script", + url: properties.script, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.serverScripts.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "serverScripts", + url: properties.serverScripts, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Image" && properties.imageURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "imageURL", + url: properties.imageURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Material" && properties.materialURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "materialURL", + url: properties.materialURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Model" && properties.modelURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "modelURL", + url: properties.modelURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if ( + (properties.type === "Zone" || properties.type === "Model" || properties.type === "ParticleEffect") + && properties.compoundShapeURL.toLowerCase().startsWith("http") + ) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "compoundShapeURL", + url: properties.compoundShapeURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Model" && properties.animation.url.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "animation.url", + url: properties.animation.url, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "ParticleEffect" && properties.textures.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "textures", + url: properties.textures, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "PolyVox" && properties.xTextureURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "xTextureURL", + url: properties.xTextureURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "PolyVox" && properties.yTextureURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "yTextureURL", + url: properties.yTextureURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "PolyVox" && properties.zTextureURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "zTextureURL", + url: properties.zTextureURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Text" && properties.font.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "font", + url: properties.font, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Web" && properties.sourceUrl.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "sourceUrl", + url: properties.sourceUrl, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Web" && properties.scriptURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "scriptURL", + url: properties.scriptURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Zone" && properties.filterURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "filterURL", + url: properties.filterURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Zone" && properties.skybox.url.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "skybox.url", + url: properties.skybox.url, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + if (properties.type === "Zone" && properties.ambientLight.ambientURL.toLowerCase().startsWith("http")) { + brokenURLReportUrlEntry = { + id: entityIDs[i], + name: properties.name, + type: properties.type, + urlType: "ambientLight.ambientURL", + url: properties.ambientLight.ambientURL, + validity: "NOT_TESTED" + }; + brokenURLReportUrlList.push(brokenURLReportUrlEntry); + } + } + if (brokenURLReportUrlList.length === 0) { + audioFeedback.confirmation(); + Window.alert("No 'http' URL has been found within the current selection."); + return; + } else { + if (brokenURLReportUrlList.length > MAX_URL_BEFORE_WARNING_FOR_LONG_PROCESS) { + var message = "Number of http URLs found: " + brokenURLReportUrlList.length + "\n The analysis may take time. Do you want to proceed?"; + var answer = Window.confirm(message); + if (!answer) { + return; + } + } + if (brokenURLReportOverlayWebWindow !== undefined) { + brokenURLReportOverlayWebWindow.close(); + } + brokenURLReportOverlayWebWindow = new OverlayWebWindow({ + title: "Broken URL Report", + source: Script.resolvePath("brokenURLReport.html"), + width: 1000, + height: 600 + }); + brokenURLReportContent = ""; + brokenURLReportRequestUrlValidityCheck(brokenURLReportProcessedUrlNo); + } + } + + brokenURLReportOverlayWebWindow.webEventReceived.connect(function (message) { + try { + var data = JSON.parse(message); + } catch(e) { + print("brokenURLReport.js: Error parsing JSON"); + return; + } + if (data.action === "select") { + selectionManager.setSelections([data.entityID], this); + } + }); +} diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index efbb531124..9b3e862210 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1969,7 +1969,7 @@ div.entity-list-menu { } div.menu-separator{ - width: 90%; + width: 100%; height: 2px; background-color: #505050; } @@ -1987,12 +1987,12 @@ button.menu-button { } button.menu-button:hover { - background-color: #00B4EF; + background-color: #919191; border: none; } button.menu-button:active { - background-color: #00B4EF; + background-color: #919191; border: none; } diff --git a/tests-manual/controllers/CMakeLists.txt b/tests-manual/controllers/CMakeLists.txt index 03043c79f2..932826c8de 100644 --- a/tests-manual/controllers/CMakeLists.txt +++ b/tests-manual/controllers/CMakeLists.txt @@ -7,6 +7,7 @@ setup_hifi_project(Script Qml) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") setup_memory_debugger() +setup_thread_debugger() # link in the shared libraries link_hifi_libraries(shared gl script-engine plugins render-utils ui-plugins input-plugins display-plugins controllers) diff --git a/tests-manual/entities/CMakeLists.txt b/tests-manual/entities/CMakeLists.txt index 6d0bf9f149..a6eed4f234 100644 --- a/tests-manual/entities/CMakeLists.txt +++ b/tests-manual/entities/CMakeLists.txt @@ -4,6 +4,7 @@ set(TARGET_NAME "entities-test") # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Network Script) setup_memory_debugger() +setup_thread_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries diff --git a/tests-manual/gl/CMakeLists.txt b/tests-manual/gl/CMakeLists.txt index 40bb64be1c..5b738a6e39 100644 --- a/tests-manual/gl/CMakeLists.txt +++ b/tests-manual/gl/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME gl-test) # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Quick Gui OpenGL) setup_memory_debugger() +setup_thread_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(shared gl) package_libraries_for_deployment() diff --git a/tests-manual/gpu-textures/CMakeLists.txt b/tests-manual/gpu-textures/CMakeLists.txt index 907690748a..d148b0cd21 100644 --- a/tests-manual/gpu-textures/CMakeLists.txt +++ b/tests-manual/gpu-textures/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME gpu-textures-tests) # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Quick Gui Script) setup_memory_debugger() +setup_thread_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries( shared shaders task networking gl diff --git a/tests-manual/gpu/CMakeLists.txt b/tests-manual/gpu/CMakeLists.txt index 6e3781acff..dc7bfbe75e 100644 --- a/tests-manual/gpu/CMakeLists.txt +++ b/tests-manual/gpu/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME gpu-test) # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Quick Gui Script) setup_memory_debugger() +setup_thread_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries( shared task networking gl diff --git a/tests-manual/qml/CMakeLists.txt b/tests-manual/qml/CMakeLists.txt index f56b8c4533..1b56927713 100644 --- a/tests-manual/qml/CMakeLists.txt +++ b/tests-manual/qml/CMakeLists.txt @@ -1,6 +1,7 @@ set(TARGET_NAME qml-test) setup_hifi_project(Quick Qml Gui OpenGL) setup_memory_debugger() +setup_thread_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(shared networking gl qml) diff --git a/tests-manual/render-perf/CMakeLists.txt b/tests-manual/render-perf/CMakeLists.txt index 9e7d826d03..67d3ac2d86 100644 --- a/tests-manual/render-perf/CMakeLists.txt +++ b/tests-manual/render-perf/CMakeLists.txt @@ -6,6 +6,7 @@ if (WIN32) endif() setup_memory_debugger() +setup_thread_debugger() # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Quick Gui) diff --git a/tests-manual/render-texture-load/CMakeLists.txt b/tests-manual/render-texture-load/CMakeLists.txt index 2e939e931a..77afdfa5d0 100644 --- a/tests-manual/render-texture-load/CMakeLists.txt +++ b/tests-manual/render-texture-load/CMakeLists.txt @@ -6,6 +6,7 @@ if (WIN32) endif() setup_memory_debugger() +setup_thread_debugger() # This is not a testcase -- just set it up as a regular hifi project setup_hifi_project(Quick Gui) diff --git a/tests-manual/render-utils/CMakeLists.txt b/tests-manual/render-utils/CMakeLists.txt index 9f575ee8ca..e5548de2b2 100644 --- a/tests-manual/render-utils/CMakeLists.txt +++ b/tests-manual/render-utils/CMakeLists.txt @@ -6,6 +6,7 @@ setup_hifi_project(Quick Gui) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") setup_memory_debugger() +setup_thread_debugger() # link in the shared libraries link_hifi_libraries(render-utils shaders gl gpu shared ${PLATFORM_GL_BACKEND}) diff --git a/tests/recording/CMakeLists.txt b/tests/recording/CMakeLists.txt index dbb942a27a..a0925aaf6d 100644 --- a/tests/recording/CMakeLists.txt +++ b/tests/recording/CMakeLists.txt @@ -3,6 +3,7 @@ set(TARGET_NAME recording-test) setup_hifi_project(Test) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") setup_memory_debugger() +setup_thread_debugger() link_hifi_libraries(shared recording) if (WIN32) target_link_libraries(${TARGET_NAME} Winmm.lib) diff --git a/tools/ac-client/CMakeLists.txt b/tools/ac-client/CMakeLists.txt index ad16187dcb..fe878b3cb4 100644 --- a/tools/ac-client/CMakeLists.txt +++ b/tools/ac-client/CMakeLists.txt @@ -1,4 +1,5 @@ set(TARGET_NAME ac-client) setup_hifi_project(Core) setup_memory_debugger() +setup_thread_debugger() link_hifi_libraries(shared networking) diff --git a/tools/atp-client/CMakeLists.txt b/tools/atp-client/CMakeLists.txt index 19c70597f7..0ed5969cb6 100644 --- a/tools/atp-client/CMakeLists.txt +++ b/tools/atp-client/CMakeLists.txt @@ -1,4 +1,5 @@ set(TARGET_NAME atp-client) setup_hifi_project(Core) setup_memory_debugger() +setup_thread_debugger() link_hifi_libraries(shared networking) diff --git a/tools/gpu-frame-player/CMakeLists.txt b/tools/gpu-frame-player/CMakeLists.txt index cb20cdde25..e3611e1068 100644 --- a/tools/gpu-frame-player/CMakeLists.txt +++ b/tools/gpu-frame-player/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME gpu-frame-player) setup_memory_debugger() +setup_thread_debugger() if (APPLE) set(CMAKE_MACOSX_BUNDLE TRUE) diff --git a/tools/ice-client/CMakeLists.txt b/tools/ice-client/CMakeLists.txt index 64ec6b131d..ebd573ebb5 100644 --- a/tools/ice-client/CMakeLists.txt +++ b/tools/ice-client/CMakeLists.txt @@ -1,4 +1,5 @@ set(TARGET_NAME ice-client) setup_hifi_project(Core) setup_memory_debugger() +setup_thread_debugger() link_hifi_libraries(shared networking) diff --git a/tools/ktx-tool/CMakeLists.txt b/tools/ktx-tool/CMakeLists.txt index 6bb09e0a9e..0c824cc2e7 100644 --- a/tools/ktx-tool/CMakeLists.txt +++ b/tools/ktx-tool/CMakeLists.txt @@ -7,6 +7,7 @@ link_hifi_libraries(shared networking image gl shaders gpu ktx) target_gli() setup_memory_debugger() +setup_thread_debugger() if (WIN32) package_libraries_for_deployment() diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index d65505415d..811cfecd94 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -31,6 +31,7 @@ source_group("UI Files" FILES ${QT_UI_FILES}) qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") setup_memory_debugger() +setup_thread_debugger() # add them to the nitpick source files set(NITPICK_SRCS ${NITPICK_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 49cf9fb6ab..9a159d8cb1 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -5,6 +5,7 @@ setup_hifi_project(Widgets Gui Concurrent) link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking procedural material-networking model-baker task) setup_memory_debugger() +setup_thread_debugger() if (WIN32) package_libraries_for_deployment() diff --git a/tools/skeleton-dump/CMakeLists.txt b/tools/skeleton-dump/CMakeLists.txt index 7d4248d171..cf95c302e8 100644 --- a/tools/skeleton-dump/CMakeLists.txt +++ b/tools/skeleton-dump/CMakeLists.txt @@ -1,6 +1,7 @@ set(TARGET_NAME skeleton-dump) setup_hifi_project(Core) setup_memory_debugger() +setup_thread_debugger() link_hifi_libraries(shared fbx hfm graphics gpu gl animation) include_hifi_library_headers(image) diff --git a/tools/vhacd-util/CMakeLists.txt b/tools/vhacd-util/CMakeLists.txt index cc8760fa2d..3c4d17122e 100644 --- a/tools/vhacd-util/CMakeLists.txt +++ b/tools/vhacd-util/CMakeLists.txt @@ -7,6 +7,7 @@ include_hifi_library_headers(image) target_vhacd() setup_memory_debugger() +setup_thread_debugger() if (WIN32) package_libraries_for_deployment()