diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 1b533f10f3..0a868737b0 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -39,7 +39,6 @@ #include "SendAssetTask.h" #include "UploadAssetTask.h" - static const uint8_t MIN_CORES_FOR_MULTICORE = 4; static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2; static const uint8_t CPU_AFFINITY_COUNT_LOW = 1; @@ -56,7 +55,7 @@ static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx"; static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx"; static const QString BAKED_SCRIPT_SIMPLE_NAME = "asset.js"; -void AssetServer::bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) { +void AssetServer::bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) { qDebug() << "Starting bake for: " << assetPath << assetHash; auto it = _pendingBakes.find(assetHash); if (it == _pendingBakes.end()) { @@ -74,23 +73,23 @@ void AssetServer::bakeAsset(const AssetHash& assetHash, const AssetPath& assetPa } } -QString AssetServer::getPathToAssetHash(const AssetHash& assetHash) { +QString AssetServer::getPathToAssetHash(const AssetUtils::AssetHash& assetHash) { return _filesDirectory.absoluteFilePath(assetHash); } -std::pair AssetServer::getAssetStatus(const AssetPath& path, const AssetHash& hash) { +std::pair AssetServer::getAssetStatus(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) { auto it = _pendingBakes.find(hash); if (it != _pendingBakes.end()) { - return { (*it)->isBaking() ? Baking : Pending, "" }; + return { (*it)->isBaking() ? AssetUtils::Baking : AssetUtils::Pending, "" }; } - if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { - return { Baked, "" }; + if (path.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { + return { AssetUtils::Baked, "" }; } auto dotIndex = path.lastIndexOf("."); if (dotIndex == -1) { - return { Irrelevant, "" }; + return { AssetUtils::Irrelevant, "" }; } auto extension = path.mid(dotIndex + 1); @@ -104,16 +103,16 @@ std::pair AssetServer::getAssetStatus(const AssetPath& pa } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) { bakedFilename = BAKED_SCRIPT_SIMPLE_NAME; } else { - return { Irrelevant, "" }; + return { AssetUtils::Irrelevant, "" }; } - auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename; + auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename; auto jt = _fileMappings.find(bakedPath); if (jt != _fileMappings.end()) { if (jt->second == hash) { - return { NotBaked, "" }; + return { AssetUtils::NotBaked, "" }; } else { - return { Baked, "" }; + return { AssetUtils::Baked, "" }; } } else { bool loaded; @@ -121,11 +120,11 @@ std::pair AssetServer::getAssetStatus(const AssetPath& pa std::tie(loaded, meta) = readMetaFile(hash); if (loaded && meta.failedLastBake) { - return { Error, meta.lastBakeErrors }; + return { AssetUtils::Error, meta.lastBakeErrors }; } } - return { Pending, "" }; + return { AssetUtils::Pending, "" }; } void AssetServer::bakeAssets() { @@ -137,14 +136,14 @@ void AssetServer::bakeAssets() { } } -void AssetServer::maybeBake(const AssetPath& path, const AssetHash& hash) { +void AssetServer::maybeBake(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) { if (needsToBeBaked(path, hash)) { qDebug() << "Queuing bake of: " << path; bakeAsset(hash, path, getPathToAssetHash(hash)); } } -void AssetServer::createEmptyMetaFile(const AssetHash& hash) { +void AssetServer::createEmptyMetaFile(const AssetUtils::AssetHash& hash) { QString metaFilePath = "atp:/" + hash + "/meta.json"; QFile metaFile { metaFilePath }; @@ -157,14 +156,14 @@ void AssetServer::createEmptyMetaFile(const AssetHash& hash) { } } -bool AssetServer::hasMetaFile(const AssetHash& hash) { - QString metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/meta.json"; +bool AssetServer::hasMetaFile(const AssetUtils::AssetHash& hash) { + QString metaFilePath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/meta.json"; return _fileMappings.find(metaFilePath) != _fileMappings.end(); } -bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHash) { - if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { +bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& assetHash) { + if (path.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { return false; } @@ -196,7 +195,7 @@ bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHa return false; } - auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename; + auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename; return _fileMappings.find(bakedPath) == _fileMappings.end(); } @@ -235,7 +234,7 @@ AssetServer::AssetServer(ReceivedMessage& message) : ThreadedAssignment(message), _transferTaskPool(this), _bakingTaskPool(this), - _filesizeLimit(MAX_UPLOAD_SIZE) + _filesizeLimit(AssetUtils::MAX_UPLOAD_SIZE) { // store the current state of image compression so we can reset it when this assignment is complete _wasColorTextureCompressionEnabled = image::isColorTexturesCompressionEnabled(); @@ -390,7 +389,7 @@ void AssetServer::completeSetup() { // Check the asset directory to output some information about what we have auto files = _filesDirectory.entryList(QDir::Files); - QRegExp hashFileRegex { ASSET_HASH_REGEX_STRING }; + QRegExp hashFileRegex { AssetUtils::ASSET_HASH_REGEX_STRING }; auto hashedFiles = files.filter(hashFileRegex); qCInfo(asset_server) << "There are" << hashedFiles.size() << "asset files in the asset directory."; @@ -410,9 +409,9 @@ void AssetServer::completeSetup() { // get file size limit for an asset static const QString ASSETS_FILESIZE_LIMIT_OPTION = "assets_filesize_limit"; auto assetsFilesizeLimitJSONValue = assetServerObject[ASSETS_FILESIZE_LIMIT_OPTION]; - auto assetsFilesizeLimit = (uint64_t)assetsFilesizeLimitJSONValue.toInt(MAX_UPLOAD_SIZE); + auto assetsFilesizeLimit = (uint64_t)assetsFilesizeLimitJSONValue.toInt(AssetUtils::MAX_UPLOAD_SIZE); - if (assetsFilesizeLimit != 0 && assetsFilesizeLimit < MAX_UPLOAD_SIZE) { + if (assetsFilesizeLimit != 0 && assetsFilesizeLimit < AssetUtils::MAX_UPLOAD_SIZE) { _filesizeLimit = assetsFilesizeLimit * BITS_PER_MEGABITS; } @@ -421,7 +420,7 @@ void AssetServer::completeSetup() { } void AssetServer::cleanupUnmappedFiles() { - QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}" }; + QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(AssetUtils::SHA256_HASH_HEX_LENGTH) + "}" }; auto files = _filesDirectory.entryInfoList(QDir::Files); @@ -454,6 +453,8 @@ void AssetServer::cleanupUnmappedFiles() { } void AssetServer::handleAssetMappingOperation(QSharedPointer message, SharedNodePointer senderNode) { + using AssetMappingOperationType = AssetUtils::AssetMappingOperationType; + MessageID messageID; message->readPrimitive(&messageID); @@ -519,7 +520,7 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode if (!bakedRootFile.isEmpty()) { // we ran into an asset for which we could have a baked version, let's check if it's ready - bakedAssetPath = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile; + bakedAssetPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile; auto bakedIt = _fileMappings.find(bakedAssetPath); if (bakedIt != _fileMappings.end()) { @@ -537,7 +538,7 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode } } - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); if (wasRedirected) { qDebug() << "Writing re-directed hash for" << originalAssetHash << "to" << redirectedAssetHash; @@ -563,12 +564,12 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode } } } else { - replyPacket.writePrimitive(AssetServerError::AssetNotFound); + replyPacket.writePrimitive(AssetUtils::AssetServerError::AssetNotFound); } } void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); uint32_t count = (uint32_t)_fileMappings.size(); @@ -580,11 +581,11 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN replyPacket.writeString(mapping); replyPacket.write(QByteArray::fromHex(hash.toUtf8())); - BakingStatus status; + AssetUtils::BakingStatus status; QString lastBakeErrors; std::tie(status, lastBakeErrors) = getAssetStatus(mapping, hash); replyPacket.writePrimitive(status); - if (status == Error) { + if (status == AssetUtils::Error) { replyPacket.writeString(lastBakeErrors); } } @@ -594,22 +595,22 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode if (senderNode->getCanWriteToAssetServer()) { QString assetPath = message.readString(); - auto assetHash = message.read(SHA256_HASH_LENGTH).toHex(); + auto assetHash = message.read(AssetUtils::SHA256_HASH_LENGTH).toHex(); // don't process a set mapping operation that is inside the hidden baked folder - if (assetPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { - qCDebug(asset_server) << "Refusing to process a set mapping operation inside" << HIDDEN_BAKED_CONTENT_FOLDER; - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + if (assetPath.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { + qCDebug(asset_server) << "Refusing to process a set mapping operation inside" << AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER; + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } else { if (setMapping(assetPath, assetHash)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -623,21 +624,21 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared for (int i = 0; i < numberOfDeletedMappings; ++i) { auto mapping = message.readString(); - if (!mapping.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + if (!mapping.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { mappingsToDelete << mapping; } else { qCDebug(asset_server) << "Refusing to delete mapping" << mapping - << "that is inside" << HIDDEN_BAKED_CONTENT_FOLDER; + << "that is inside" << AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER; } } if (deleteMappings(mappingsToDelete)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -646,20 +647,20 @@ void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedN QString oldPath = message.readString(); QString newPath = message.readString(); - if (oldPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER) || newPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + if (oldPath.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER) || newPath.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { qCDebug(asset_server) << "Cannot rename" << oldPath << "to" << newPath - << "since one of the paths is inside" << HIDDEN_BAKED_CONTENT_FOLDER; - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + << "since one of the paths is inside" << AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER; + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } else { if (renameMapping(oldPath, newPath)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -678,12 +679,12 @@ void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, Shar } if (setBakingEnabled(mappings, enabled)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -691,15 +692,15 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh QByteArray assetHash; MessageID messageID; - if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID))) { + if (message->getSize() < qint64(AssetUtils::SHA256_HASH_LENGTH + sizeof(messageID))) { qCDebug(asset_server) << "ERROR bad file request"; return; } message->readPrimitive(&messageID); - assetHash = message->readWithoutCopy(SHA256_HASH_LENGTH); + assetHash = message->readWithoutCopy(AssetUtils::SHA256_HASH_LENGTH); - auto size = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(AssetServerError) + sizeof(qint64)); + auto size = qint64(sizeof(MessageID) + AssetUtils::SHA256_HASH_LENGTH + sizeof(AssetUtils::AssetServerError) + sizeof(qint64)); auto replyPacket = NLPacket::create(PacketType::AssetGetInfoReply, size, true); QByteArray hexHash = assetHash.toHex(); @@ -712,11 +713,11 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh if (fileInfo.exists() && fileInfo.isReadable()) { qCDebug(asset_server) << "Opening file: " << fileInfo.filePath(); - replyPacket->writePrimitive(AssetServerError::NoError); + replyPacket->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacket->writePrimitive(fileInfo.size()); } else { qCDebug(asset_server) << "Asset not found: " << QString(hexHash); - replyPacket->writePrimitive(AssetServerError::AssetNotFound); + replyPacket->writePrimitive(AssetUtils::AssetServerError::AssetNotFound); } auto nodeList = DependencyManager::get(); @@ -725,7 +726,7 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh void AssetServer::handleAssetGet(QSharedPointer message, SharedNodePointer senderNode) { - auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(DataOffset) + sizeof(DataOffset)); + auto minSize = qint64(sizeof(MessageID) + AssetUtils::SHA256_HASH_LENGTH + sizeof(AssetUtils::DataOffset) + sizeof(AssetUtils::DataOffset)); if (message->getSize() < minSize) { qCDebug(asset_server) << "ERROR bad file request"; @@ -749,14 +750,14 @@ void AssetServer::handleAssetUpload(QSharedPointer message, Sha // for now this also means it isn't allowed to add assets // so return a packet with error that indicates that - auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetServerError), true); + auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetUtils::AssetServerError), true); MessageID messageID; message->readPrimitive(&messageID); // write the message ID and a permission denied error permissionErrorPacket->writePrimitive(messageID); - permissionErrorPacket->writePrimitive(AssetServerError::PermissionDenied); + permissionErrorPacket->writePrimitive(AssetUtils::AssetServerError::PermissionDenied); // send off the packet auto nodeList = DependencyManager::get(); @@ -863,12 +864,12 @@ bool AssetServer::loadMappingsFromFile() { continue; } - if (!isValidFilePath(key)) { + if (!AssetUtils::isValidFilePath(key)) { qCWarning(asset_server) << "Will not keep mapping for" << key << "since it is not a valid path."; continue; } - if (!isValidHash(value.toString())) { + if (!AssetUtils::isValidHash(value.toString())) { qCWarning(asset_server) << "Will not keep mapping for" << key << "since it does not have a valid hash."; continue; } @@ -918,15 +919,15 @@ bool AssetServer::writeMappingsToFile() { return false; } -bool AssetServer::setMapping(AssetPath path, AssetHash hash) { +bool AssetServer::setMapping(AssetUtils::AssetPath path, AssetUtils::AssetHash hash) { path = path.trimmed(); - if (!isValidFilePath(path)) { + if (!AssetUtils::isValidFilePath(path)) { qCWarning(asset_server) << "Cannot set a mapping for invalid path:" << path << "=>" << hash; return false; } - if (!isValidHash(hash)) { + if (!AssetUtils::isValidHash(hash)) { qCWarning(asset_server) << "Cannot set a mapping for invalid hash" << path << "=>" << hash; return false; } @@ -958,23 +959,23 @@ bool AssetServer::setMapping(AssetPath path, AssetHash hash) { } } -bool pathIsFolder(const AssetPath& path) { +bool pathIsFolder(const AssetUtils::AssetPath& path) { return path.endsWith('/'); } -void AssetServer::removeBakedPathsForDeletedAsset(AssetHash hash) { +void AssetServer::removeBakedPathsForDeletedAsset(AssetUtils::AssetHash hash) { // we deleted the file with this hash // check if we had baked content for that file that should also now be removed // by calling deleteMappings for the hidden baked content folder for this hash - AssetPathList hiddenBakedFolder { HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" }; + AssetUtils::AssetPathList hiddenBakedFolder { AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" }; qCDebug(asset_server) << "Deleting baked content below" << hiddenBakedFolder << "since" << hash << "was deleted"; deleteMappings(hiddenBakedFolder); } -bool AssetServer::deleteMappings(const AssetPathList& paths) { +bool AssetServer::deleteMappings(const AssetUtils::AssetPathList& paths) { // take a copy of the current mappings in case persistence of these deletes fails auto oldMappings = _fileMappings; @@ -1060,11 +1061,11 @@ bool AssetServer::deleteMappings(const AssetPathList& paths) { } } -bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { +bool AssetServer::renameMapping(AssetUtils::AssetPath oldPath, AssetUtils::AssetPath newPath) { oldPath = oldPath.trimmed(); newPath = newPath.trimmed(); - if (!isValidFilePath(oldPath) || !isValidFilePath(newPath)) { + if (!AssetUtils::isValidFilePath(oldPath) || !AssetUtils::isValidFilePath(newPath)) { qCWarning(asset_server) << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:" << oldPath << "=>" << newPath; @@ -1164,8 +1165,8 @@ static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx"; static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx"; static const QString BAKED_ASSET_SIMPLE_JS_NAME = "asset.js"; -QString getBakeMapping(const AssetHash& hash, const QString& relativeFilePath) { - return HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; +QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativeFilePath) { + return AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; } void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) { @@ -1197,7 +1198,7 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina qDebug() << "File path: " << filePath; - AssetHash bakedFileHash; + AssetUtils::AssetHash bakedFileHash; if (file.open(QIODevice::ReadOnly)) { QCryptographicHash hasher(QCryptographicHash::Sha256); @@ -1290,8 +1291,8 @@ static const QString BAKE_VERSION_KEY = "bake_version"; static const QString FAILED_LAST_BAKE_KEY = "failed_last_bake"; static const QString LAST_BAKE_ERRORS_KEY = "last_bake_errors"; -std::pair AssetServer::readMetaFile(AssetHash hash) { - auto metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json"; +std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) { + auto metaFilePath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json"; auto it = _fileMappings.find(metaFilePath); if (it == _fileMappings.end()) { @@ -1335,7 +1336,7 @@ std::pair AssetServer::readMetaFile(AssetHash hash) { return { false, {} }; } -bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta) { +bool AssetServer::writeMetaFile(AssetUtils::AssetHash originalAssetHash, const AssetMeta& meta) { // construct the JSON that will be in the meta file QJsonObject metaFileObject; @@ -1349,7 +1350,7 @@ bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& me auto metaFileJSON = metaFileDoc.toJson(); // get a hash for the contents of the meta-file - AssetHash metaFileHash = QCryptographicHash::hash(metaFileJSON, QCryptographicHash::Sha256).toHex(); + AssetUtils::AssetHash metaFileHash = QCryptographicHash::hash(metaFileJSON, QCryptographicHash::Sha256).toHex(); // create the meta file in our files folder, named by the hash of its contents QFile metaFile(_filesDirectory.absoluteFilePath(metaFileHash)); @@ -1359,7 +1360,7 @@ bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& me metaFile.close(); // add a mapping to the meta file so it doesn't get deleted because it is unmapped - auto metaFileMapping = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + "meta.json"; + auto metaFileMapping = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + "meta.json"; return setMapping(metaFileMapping, metaFileHash); } else { @@ -1367,7 +1368,7 @@ bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& me } } -bool AssetServer::setBakingEnabled(const AssetPathList& paths, bool enabled) { +bool AssetServer::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled) { for (const auto& path : paths) { auto it = _fileMappings.find(path); if (it != _fileMappings.end()) { diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index e6393e6a98..5e7a3c1700 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -21,7 +21,6 @@ #include "AssetUtils.h" #include "ReceivedMessage.h" - namespace std { template <> struct hash { @@ -75,29 +74,29 @@ private: bool writeMappingsToFile(); /// Set the mapping for path to hash - bool setMapping(AssetPath path, AssetHash hash); + bool setMapping(AssetUtils::AssetPath path, AssetUtils::AssetHash hash); /// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`. - bool deleteMappings(const AssetPathList& paths); + bool deleteMappings(const AssetUtils::AssetPathList& paths); /// Rename mapping from `oldPath` to `newPath`. Returns true if successful - bool renameMapping(AssetPath oldPath, AssetPath newPath); + bool renameMapping(AssetUtils::AssetPath oldPath, AssetUtils::AssetPath newPath); - bool setBakingEnabled(const AssetPathList& paths, bool enabled); + bool setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled); /// Delete any unmapped files from the local asset directory void cleanupUnmappedFiles(); - QString getPathToAssetHash(const AssetHash& assetHash); + QString getPathToAssetHash(const AssetUtils::AssetHash& assetHash); - std::pair getAssetStatus(const AssetPath& path, const AssetHash& hash); + std::pair getAssetStatus(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); void bakeAssets(); - void maybeBake(const AssetPath& path, const AssetHash& hash); - void createEmptyMetaFile(const AssetHash& hash); - bool hasMetaFile(const AssetHash& hash); - bool needsToBeBaked(const AssetPath& path, const AssetHash& assetHash); - void bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath); + void maybeBake(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); + void createEmptyMetaFile(const AssetUtils::AssetHash& hash); + bool hasMetaFile(const AssetUtils::AssetHash& hash); + bool needsToBeBaked(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& assetHash); + void bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath); /// Move baked content for asset to baked directory and update baked status void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir, @@ -106,11 +105,11 @@ private: void handleAbortedBake(QString originalAssetHash, QString assetPath); /// Create meta file to describe baked content for original asset - std::pair readMetaFile(AssetHash hash); - bool writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta()); + std::pair readMetaFile(AssetUtils::AssetHash hash); + bool writeMetaFile(AssetUtils::AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta()); /// Remove baked paths when the original asset is deleteds - void removeBakedPathsForDeletedAsset(AssetHash originalAssetHash); + void removeBakedPathsForDeletedAsset(AssetUtils::AssetHash originalAssetHash); Mappings _fileMappings; @@ -120,7 +119,7 @@ private: /// Task pool for handling uploads and downloads of assets QThreadPool _transferTaskPool; - QHash> _pendingBakes; + QHash> _pendingBakes; QThreadPool _bakingTaskPool; bool _wasColorTextureCompressionEnabled { false }; diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index 49322ca4cb..5b9add7467 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -24,7 +24,7 @@ static const int OVEN_STATUS_CODE_ABORT { 2 }; std::once_flag registerMetaTypesFlag; -BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) : +BakeAssetTask::BakeAssetTask(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) : _assetHash(assetHash), _assetPath(assetPath), _filePath(filePath) diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h index c73a8bff65..7b003fa1bd 100644 --- a/assignment-client/src/assets/BakeAssetTask.h +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -25,7 +25,7 @@ class BakeAssetTask : public QObject, public QRunnable { Q_OBJECT public: - BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath); + BakeAssetTask(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath); bool isBaking() { return _isBaking.load(); } @@ -41,8 +41,8 @@ signals: private: std::atomic _isBaking { false }; - AssetHash _assetHash; - AssetPath _assetPath; + AssetUtils::AssetHash _assetHash; + AssetUtils::AssetPath _assetPath; QString _filePath; std::unique_ptr _ovenProcess { nullptr }; std::atomic _wasAborted { false }; diff --git a/assignment-client/src/assets/SendAssetTask.cpp b/assignment-client/src/assets/SendAssetTask.cpp index eab88e0d46..6da092357f 100644 --- a/assignment-client/src/assets/SendAssetTask.cpp +++ b/assignment-client/src/assets/SendAssetTask.cpp @@ -40,7 +40,7 @@ void SendAssetTask::run() { ByteRange byteRange; _message->readPrimitive(&messageID); - QByteArray assetHash = _message->read(SHA256_HASH_LENGTH); + QByteArray assetHash = _message->read(AssetUtils::SHA256_HASH_LENGTH); // `start` and `end` indicate the range of data to retrieve for the asset identified by `assetHash`. // `start` is inclusive, `end` is exclusive. Requesting `start` = 1, `end` = 10 will retrieve 9 bytes of data, @@ -61,7 +61,7 @@ void SendAssetTask::run() { replyPacketList->writePrimitive(messageID); if (!byteRange.isValid()) { - replyPacketList->writePrimitive(AssetServerError::InvalidByteRange); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::InvalidByteRange); } else { QString filePath = _resourcesDir.filePath(QString(hexHash)); @@ -75,7 +75,7 @@ void SendAssetTask::run() { // check if we're being asked to read data that we just don't have // because of the file size if (file.size() < byteRange.fromInclusive || file.size() < byteRange.toExclusive) { - replyPacketList->writePrimitive(AssetServerError::InvalidByteRange); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::InvalidByteRange); qCDebug(networking) << "Bad byte range: " << hexHash << " " << byteRange.fromInclusive << ":" << byteRange.toExclusive; } else { @@ -86,7 +86,7 @@ void SendAssetTask::run() { // this range is positive, meaning we just need to seek into the file and then read from there file.seek(byteRange.fromInclusive); - replyPacketList->writePrimitive(AssetServerError::NoError); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacketList->writePrimitive(size); replyPacketList->write(file.read(size)); } else { @@ -95,7 +95,7 @@ void SendAssetTask::run() { // seek to the part of the file where the negative range begins file.seek(file.size() + byteRange.fromInclusive); - replyPacketList->writePrimitive(AssetServerError::NoError); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacketList->writePrimitive(size); // first write everything from the negative range to the end of the file @@ -107,7 +107,7 @@ void SendAssetTask::run() { file.close(); } else { qCDebug(networking) << "Asset not found: " << filePath << "(" << hexHash << ")"; - replyPacketList->writePrimitive(AssetServerError::AssetNotFound); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::AssetNotFound); } } diff --git a/assignment-client/src/assets/UploadAssetTask.cpp b/assignment-client/src/assets/UploadAssetTask.cpp index 5e6d59d032..68bf4db5fd 100644 --- a/assignment-client/src/assets/UploadAssetTask.cpp +++ b/assignment-client/src/assets/UploadAssetTask.cpp @@ -20,7 +20,6 @@ #include "ClientServerUtils.h" - UploadAssetTask::UploadAssetTask(QSharedPointer receivedMessage, SharedNodePointer senderNode, const QDir& resourcesDir, uint64_t filesizeLimit) : _receivedMessage(receivedMessage), @@ -50,11 +49,11 @@ void UploadAssetTask::run() { replyPacket->writePrimitive(messageID); if (fileSize > _filesizeLimit) { - replyPacket->writePrimitive(AssetServerError::AssetTooLarge); + replyPacket->writePrimitive(AssetUtils::AssetServerError::AssetTooLarge); } else { QByteArray fileData = buffer.read(fileSize); - auto hash = hashData(fileData); + auto hash = AssetUtils::hashData(fileData); auto hexHash = hash.toHex(); qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID()) @@ -66,12 +65,12 @@ void UploadAssetTask::run() { if (file.exists()) { // check if the local file has the correct contents, otherwise we overwrite - if (file.open(QIODevice::ReadOnly) && hashData(file.readAll()) == hash) { + if (file.open(QIODevice::ReadOnly) && AssetUtils::hashData(file.readAll()) == hash) { qDebug() << "Not overwriting existing verified file: " << hexHash; existingCorrectFile = true; - replyPacket->writePrimitive(AssetServerError::NoError); + replyPacket->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacket->write(hash); } else { qDebug() << "Overwriting an existing file whose contents did not match the expected hash: " << hexHash; @@ -84,7 +83,7 @@ void UploadAssetTask::run() { qDebug() << "Wrote file" << hexHash << "to disk. Upload complete"; file.close(); - replyPacket->writePrimitive(AssetServerError::NoError); + replyPacket->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacket->write(hash); } else { qWarning() << "Failed to upload or write to file" << hexHash << " - upload failed."; @@ -96,7 +95,7 @@ void UploadAssetTask::run() { qWarning() << "Removal of failed upload file" << hexHash << "failed."; } - replyPacket->writePrimitive(AssetServerError::FileOperationFailed); + replyPacket->writePrimitive(AssetUtils::AssetServerError::FileOperationFailed); } } diff --git a/interface/resources/qml/hifi/commerce/common/SortableListModel.qml b/interface/resources/qml/hifi/commerce/common/SortableListModel.qml index 2d82e42ddb..cfdd4abe04 100644 --- a/interface/resources/qml/hifi/commerce/common/SortableListModel.qml +++ b/interface/resources/qml/hifi/commerce/common/SortableListModel.qml @@ -17,11 +17,12 @@ ListModel { id: root; property string sortColumnName: ""; property bool isSortingDescending: true; + property bool valuesAreNumerical: false; function swap(a, b) { if (a < b) { move(a, b, 1); - move (b - 1, a, 1); + move(b - 1, a, 1); } else if (a > b) { move(b, a, 1); move(a - 1, b, 1); @@ -29,26 +30,53 @@ ListModel { } function partition(begin, end, pivot) { - var piv = get(pivot)[sortColumnName]; - swap(pivot, end - 1); - var store = begin; + if (valuesAreNumerical) { + var piv = get(pivot)[sortColumnName]; + swap(pivot, end - 1); + var store = begin; + var i; - for (var i = begin; i < end - 1; ++i) { - if (isSortingDescending) { - if (get(i)[sortColumnName] < piv) { - swap(store, i); - ++store; - } - } else { - if (get(i)[sortColumnName] > piv) { - swap(store, i); - ++store; + for (i = begin; i < end - 1; ++i) { + var currentElement = get(i)[sortColumnName]; + if (isSortingDescending) { + if (currentElement > piv) { + swap(store, i); + ++store; + } + } else { + if (currentElement < piv) { + swap(store, i); + ++store; + } } } - } - swap(end - 1, store); + swap(end - 1, store); - return store; + return store; + } else { + var piv = get(pivot)[sortColumnName].toLowerCase(); + swap(pivot, end - 1); + var store = begin; + var i; + + for (i = begin; i < end - 1; ++i) { + var currentElement = get(i)[sortColumnName].toLowerCase(); + if (isSortingDescending) { + if (currentElement > piv) { + swap(store, i); + ++store; + } + } else { + if (currentElement < piv) { + swap(store, i); + ++store; + } + } + } + swap(end - 1, store); + + return store; + } } function qsort(begin, end) { diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 87b784bc4e..2743677683 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -317,6 +317,7 @@ Rectangle { HifiControlsUit.TextField { id: filterBar; + property string previousText: ""; colorScheme: hifi.colorSchemes.faintGray; hasClearButton: true; hasRoundedBorder: true; @@ -329,6 +330,8 @@ Rectangle { onTextChanged: { buildFilteredPurchasesModel(); + purchasesContentsList.positionViewAtIndex(0, ListView.Beginning) + filterBar.previousText = filterBar.text; } onAccepted: { @@ -647,7 +650,8 @@ Rectangle { function sortByDate() { filteredPurchasesModel.sortColumnName = "purchase_date"; - filteredPurchasesModel.isSortingDescending = false; + filteredPurchasesModel.isSortingDescending = true; + filteredPurchasesModel.valuesAreNumerical = true; filteredPurchasesModel.quickSort(); } @@ -677,7 +681,7 @@ Rectangle { } } - if (sameItemCount !== tempPurchasesModel.count) { + if (sameItemCount !== tempPurchasesModel.count || filterBar.text !== filterBar.previousText) { filteredPurchasesModel.clear(); for (var i = 0; i < tempPurchasesModel.count; i++) { filteredPurchasesModel.append(tempPurchasesModel.get(i)); diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml index d825196655..634a68d117 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -193,7 +193,7 @@ Item { color: hifi.colors.white; } - // "Change Passphrase" button + // "Change Security Pic" button HifiControlsUit.Button { id: changeSecurityImageButton; color: hifi.buttons.blue; diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml index 2ad2b75553..86a4220b74 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml @@ -34,13 +34,11 @@ Item { securityImageChangePageSecurityImage.source = "image://security/securityImage"; if (exists) { // Success submitting new security image if (root.justSubmitted) { - root.resetSubmitButton(); sendSignalToWallet({method: "walletSecurity_changeSecurityImageSuccess"}); root.justSubmitted = false; } } else if (root.justSubmitted) { // Error submitting new security image. - root.resetSubmitButton(); root.justSubmitted = false; } } @@ -180,7 +178,8 @@ Item { // "Submit" button HifiControlsUit.Button { id: securityImageSubmitButton; - enabled: securityImageSelection.currentIndex !== -1; + text: root.justSubmitted ? "Submitting..." : "Submit"; + enabled: securityImageSelection.currentIndex !== -1 && !root.justSubmitted; color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; @@ -188,11 +187,8 @@ Item { anchors.right: parent.right; anchors.rightMargin: 20; width: 150; - text: "Submit"; onClicked: { root.justSubmitted = true; - securityImageSubmitButton.text = "Submitting..."; - securityImageSubmitButton.enabled = false; var securityImagePath = securityImageSelection.getImagePathFromImageID(securityImageSelection.getSelectedImageIndex()) Commerce.chooseSecurityImage(securityImagePath); } @@ -205,11 +201,6 @@ Item { signal sendSignalToWallet(var msg); - function resetSubmitButton() { - securityImageSubmitButton.enabled = true; - securityImageSubmitButton.text = "Submit"; - } - function initModel() { securityImageSelection.initModel(); } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml index 1f5e67eaa5..56b78c5865 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -24,7 +24,7 @@ Item { HifiConstants { id: hifi; } id: root; - property int currentIndex: securityImageGrid.currentIndex; + property alias currentIndex: securityImageGrid.currentIndex; // This will cause a bug -- if you bring up security image selection in HUD mode while // in HMD while having HMD preview enabled, then move, then finish passphrase selection, @@ -98,6 +98,11 @@ Item { function initModel() { gridModel.initModel(); + securityImageGrid.currentIndex = -1; + } + + function resetSelection() { + securityImageGrid.currentIndex = -1; } // // FUNCTION DEFINITIONS END diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index 21da38def0..fd74b07465 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -348,6 +348,7 @@ Item { width: 200; text: "Back" onClicked: { + securityImageSelection.resetSelection(); root.activeView = "step_1"; } } @@ -516,6 +517,7 @@ Item { width: 200; text: "Back" onClicked: { + securityImageSelection.resetSelection(); root.lastPage = "step_3"; root.activeView = "step_2"; } diff --git a/interface/resources/qml/hifi/dialogs/AdvancedPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/AdvancedPreferencesDialog.qml new file mode 100644 index 0000000000..2d80a1c330 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/AdvancedPreferencesDialog.qml @@ -0,0 +1,29 @@ +// +// AdvancedPreferencesDialog.qml +// +// Created by Brad Hefta-Gaub on 20 Jan 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import Qt.labs.settings 1.0 + +import "../../dialogs" + +PreferencesDialog { + id: root + objectName: "AdvancedPreferencesDialog" + title: "Advanced Settings" + showCategories: ["Advanced UI" ] + property var settings: Settings { + category: root.objectName + property alias x: root.x + property alias y: root.y + property alias width: root.width + property alias height: root.height + } +} + diff --git a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml index 94665794d6..6f5798e2b2 100644 --- a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml @@ -17,7 +17,7 @@ PreferencesDialog { id: root objectName: "GeneralPreferencesDialog" title: "General Settings" - showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Game Controller", "Sixense Controllers", "Perception Neuron", "Kinect", "Leap Motion"] + showCategories: ["UI", "Snapshots", "Privacy", "HMD", "Game Controller", "Sixense Controllers", "Perception Neuron", "Kinect", "Leap Motion"] property var settings: Settings { category: root.objectName property alias x: root.x diff --git a/interface/resources/styles/global.qss b/interface/resources/styles/global.qss index 2554f3b2c9..778e5759b3 100644 --- a/interface/resources/styles/global.qss +++ b/interface/resources/styles/global.qss @@ -41,14 +41,14 @@ QSpinBox, QDoubleSpinBox { QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { - background-image: url(styles/up.svg); + background-image: url(:/styles/up.svg); background-repeat: no-repeat; background-position: center center; } QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { - background-image: url(styles/down.svg); + background-image: url(:/styles/down.svg); background-repeat: no-repeat; background-position: center center; } @@ -88,7 +88,7 @@ QSlider { QSlider::groove:horizontal { border: none; - background-image: url(styles/slider-bg.svg); + background-image: url(:/styles/slider-bg.svg); background-repeat: no-repeat; background-position: center center; } @@ -96,7 +96,7 @@ QSlider::groove:horizontal { QSlider::handle:horizontal { width: 18px; height: 18px; - background-image: url(styles/slider-handle.svg); + background-image: url(:/styles/slider-handle.svg); background-repeat: no-repeat; background-position: center center; } @@ -107,7 +107,7 @@ QPushButton#closeButton { border-width: 1px; border-radius: 0; background-color: #fff; - background-image: url(styles/close.svg); + background-image: url(:/styles/close.svg); background-repeat: no-repeat; background-position: center center; } diff --git a/interface/resources/styles/import_dialog.qss b/interface/resources/styles/import_dialog.qss index 8fe04ae1b7..3c2dbdcce9 100644 --- a/interface/resources/styles/import_dialog.qss +++ b/interface/resources/styles/import_dialog.qss @@ -63,17 +63,17 @@ QPushButton#cancelButton { } #backButton { - background-image: url(icons/backButton.svg); + background-image: url(:/icons/backButton.svg); border-radius: 0px; } #forwardButton { - background-image: url(icons/forwardButton.svg); + background-image: url(:/icons/forwardButton.svg); border-radius: 0px; } #toParentButton { - background-image: url(icons/toParentButton.svg); + background-image: url(:/icons/toParentButton.svg); border-radius: 0px; } diff --git a/interface/resources/styles/log_dialog.qss b/interface/resources/styles/log_dialog.qss index 33473d2903..e0ec17549d 100644 --- a/interface/resources/styles/log_dialog.qss +++ b/interface/resources/styles/log_dialog.qss @@ -22,7 +22,7 @@ QLineEdit { } QPushButton#searchButton { - background: url(styles/search.svg); + background: url(:/styles/search.svg); background-repeat: none; background-position: left center; background-origin: content; @@ -55,7 +55,7 @@ QPushButton#searchPrevButton { QPushButton#revealLogButton { font-family: Helvetica, Arial, sans-serif; - background: url(styles/txt-file.svg); + background: url(:/styles/txt-file.svg); background-repeat: none; background-position: left center; background-origin: content; @@ -86,11 +86,11 @@ QCheckBox { } QCheckBox::indicator:unchecked { - image: url(styles/unchecked.svg); + image: url(:/styles/unchecked.svg); } QCheckBox::indicator:checked { - image: url(styles/checked.svg); + image: url(:/styles/checked.svg); } QComboBox { @@ -110,6 +110,6 @@ QComboBox::drop-down { } QComboBox::down-arrow { - image: url(styles/filter.png); + image: url(:/styles/filter.png); border-width: 0px; } \ No newline at end of file diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4cf09a56c3..64e9feea02 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -351,7 +352,7 @@ static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; -static const QString INFO_HELP_PATH = "../../../html/tabletHelp.html"; +static const QString INFO_HELP_PATH = "html/tabletHelp.html"; static const unsigned int THROTTLED_SIM_FRAMERATE = 15; static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; @@ -763,6 +764,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -1436,8 +1438,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); } - // force the model the look at the correct directory (weird order of operations issue) - scriptEngines->setScriptsLocation(scriptEngines->getScriptsLocation()); + // this will force the model the look at the correct directory (weird order of operations issue) + scriptEngines->reloadLocalFiles(); + // do this as late as possible so that all required subsystems are initialized // If we've overridden the default scripts location, just load default scripts // otherwise, load 'em all @@ -1905,7 +1908,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo entityResult.distance = pickResult->distance; entityResult.surfaceNormal = pickResult->surfaceNormal; entityResult.entityID = pickResult->objectID; - entityResult.entity = DependencyManager::get()->getTree()->findEntityByID(entityResult.entityID); + entityResult.extraInfo = pickResult->extraInfo; } } return entityResult; @@ -2442,6 +2445,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarEntitiesBookmarks", DependencyManager::get().data()); surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); // Caches @@ -2723,7 +2727,8 @@ void Application::showHelp() { queryString.addQueryItem("defaultTab", defaultTab); auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - tablet->gotoWebScreen(INFO_HELP_PATH + "?" + queryString.toString()); + tablet->gotoWebScreen(PathUtils::resourcesUrl() + INFO_HELP_PATH + "?" + queryString.toString()); + DependencyManager::get()->openTablet(); //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); } @@ -5849,6 +5854,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AvatarEntitiesBookmarks", DependencyManager::get().data()); scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); diff --git a/interface/src/AvatarEntitiesBookmarks.cpp b/interface/src/AvatarEntitiesBookmarks.cpp new file mode 100644 index 0000000000..d108bf3a23 --- /dev/null +++ b/interface/src/AvatarEntitiesBookmarks.cpp @@ -0,0 +1,168 @@ +// +// AvatarEntitiesBookmarks.cpp +// interface/src +// +// Created by Dante Ruiz on 15/01/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MainWindow.h" +#include "Menu.h" +#include "AvatarEntitiesBookmarks.h" +#include "InterfaceLogging.h" + +#include "QVariantGLM.h" + +#include + +void addAvatarEntities(const QVariantList& avatarEntities) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + EntityTreePointer entityTree = DependencyManager::get()->getTree(); + if (!entityTree) { + return; + } + EntitySimulationPointer entitySimulation = entityTree->getSimulation(); + PhysicalEntitySimulationPointer physicalEntitySimulation = std::static_pointer_cast(entitySimulation); + EntityEditPacketSender* entityPacketSender = physicalEntitySimulation->getPacketSender(); + QScriptEngine scriptEngine; + for (int index = 0; index < avatarEntities.count(); index++) { + const QVariantMap& avatarEntityProperties = avatarEntities.at(index).toMap(); + QVariant variantProperties = avatarEntityProperties["properties"]; + QVariantMap asMap = variantProperties.toMap(); + QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); + EntityItemProperties entityProperties; + EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, entityProperties); + + entityProperties.setParentID(myNodeID); + entityProperties.setClientOnly(true); + entityProperties.setOwningAvatarID(myNodeID); + entityProperties.setSimulationOwner(myNodeID, AVATAR_ENTITY_SIMULATION_PRIORITY); + entityProperties.markAllChanged(); + + EntityItemID id = EntityItemID(QUuid::createUuid()); + bool success = true; + entityTree->withWriteLock([&] { + EntityItemPointer entity = entityTree->addEntity(id, entityProperties); + if (entity) { + if (entityProperties.queryAACubeRelatedPropertyChanged()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + bool success; + AACube queryAACube = entity->getQueryAACube(success); + if (success) { + entityProperties.setQueryAACube(queryAACube); + } + } + + entity->setLastBroadcast(usecTimestampNow()); + // since we're creating this object we will immediately volunteer to own its simulation + entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); + entityProperties.setLastEdited(entity->getLastEdited()); + } else { + qCDebug(entities) << "AvatarEntitiesBookmark failed to add new Entity to local Octree"; + success = false; + } + }); + + if (success) { + entityPacketSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, entityProperties); + } + } +} + +AvatarEntitiesBookmarks::AvatarEntitiesBookmarks() { + _bookmarksFilename = PathUtils::getAppDataPath() + "/" + AVATAR_ENTITIES_BOOKMARKS_FILENAME; + Bookmarks::readFromFile(); +} + +void AvatarEntitiesBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { + auto bookmarkAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::BookmarkAvatarEntities); + QObject::connect(bookmarkAction, SIGNAL(triggered()), this, SLOT(addBookmark()), Qt::QueuedConnection); + _bookmarksMenu = menu->addMenu(MenuOption::AvatarEntitiesBookmarks); + _deleteBookmarksAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::DeleteAvatarEntitiesBookmark); + QObject::connect(_deleteBookmarksAction, SIGNAL(triggered()), this, SLOT(deleteBookmark()), Qt::QueuedConnection); + + for (auto it = _bookmarks.begin(); it != _bookmarks.end(); ++it) { + addBookmarkToMenu(menubar, it.key(), it.value()); + } + + Bookmarks::sortActions(menubar, _bookmarksMenu); +} + +void AvatarEntitiesBookmarks::applyBookmarkedAvatarEntities() { + QAction* action = qobject_cast(sender()); + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + const QMap bookmark = action->data().toMap(); + + if (bookmark.value(ENTRY_VERSION) == AVATAR_ENTITIES_BOOKMARK_VERSION) { + myAvatar->removeAvatarEntities(); + const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); + myAvatar->useFullAvatarURL(avatarUrl); + const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); + addAvatarEntities(avatarEntities); + const float& avatarScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); + myAvatar->setAvatarScale(avatarScale); + } else { + qCDebug(interfaceapp) << " Bookmark entry does not match client version, make sure client has a handler for the new AvatarEntitiesBookmark"; + } +} + +void AvatarEntitiesBookmarks::addBookmark() { + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar Entities", "Name", QString()); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + auto bookmarkName = response.toString(); + bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); + if (bookmarkName.length() == 0) { + return; + } + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); + const QVariant& avatarScale = myAvatar->getAvatarScale(); + + QVariantMap *bookmark = new QVariantMap; + bookmark->insert(ENTRY_VERSION, AVATAR_ENTITIES_BOOKMARK_VERSION); + bookmark->insert(ENTRY_AVATAR_URL, avatarUrl); + bookmark->insert(ENTRY_AVATAR_SCALE, avatarScale); + bookmark->insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); + + Bookmarks::addBookmarkToFile(bookmarkName, *bookmark); + }); +} + +void AvatarEntitiesBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { + QAction* changeAction = _bookmarksMenu->newAction(); + changeAction->setData(bookmark); + connect(changeAction, SIGNAL(triggered()), this, SLOT(applyBookmarkedAvatarEntities())); + if (!_isMenuSorted) { + menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); + } else { + // TODO: this is aggressive but other alternatives have proved less fruitful so far. + menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); + Bookmarks::sortActions(menubar, _bookmarksMenu); + } +} diff --git a/interface/src/AvatarEntitiesBookmarks.h b/interface/src/AvatarEntitiesBookmarks.h new file mode 100644 index 0000000000..0c70e4dbc0 --- /dev/null +++ b/interface/src/AvatarEntitiesBookmarks.h @@ -0,0 +1,45 @@ +// +// AvatarEntitiesBookmarks.h +// interface/src +// +// Created by Dante Ruiz on 15/01/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AvatarEntitiesBookmarks_h +#define hifi_AvatarEntitiesBookmarks_h + +#include +#include "Bookmarks.h" + +class AvatarEntitiesBookmarks: public Bookmarks, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + AvatarEntitiesBookmarks(); + void setupMenus(Menu* menubar, MenuWrapper* menu) override; + +public slots: + void addBookmark(); + +protected: + void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; + +private: + const QString AVATAR_ENTITIES_BOOKMARKS_FILENAME = "AvatarEntitiesBookmarks.json"; + const QString ENTRY_AVATAR_URL = "AvatarUrl"; + const QString ENTRY_AVATAR_SCALE = "AvatarScale"; + const QString ENTRY_AVATAR_ENTITIES = "AvatarEntities"; + const QString ENTRY_VERSION = "version"; + + const int AVATAR_ENTITIES_BOOKMARK_VERSION = 1; + +private slots: + void applyBookmarkedAvatarEntities(); +}; + +#endif diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 2a33ee6f7b..50e25287f1 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -34,6 +34,7 @@ #include "audio/AudioScope.h" #include "avatar/AvatarManager.h" #include "AvatarBookmarks.h" +#include "AvatarEntitiesBookmarks.h" #include "devices/DdeFaceTracker.h" #include "MainWindow.h" #include "render/DrawStatus.h" @@ -206,6 +207,9 @@ Menu::Menu() { auto avatarBookmarks = DependencyManager::get(); avatarBookmarks->setupMenus(this, avatarMenu); + auto avatarEntitiesBookmarks = DependencyManager::get(); + avatarEntitiesBookmarks->setupMenus(this, avatarMenu); + // Display menu ---------------------------------- // FIXME - this is not yet matching Alan's spec because it doesn't have // menus for "2D"/"3D" - we need to add support for detecting the appropriate @@ -756,6 +760,13 @@ Menu::Menu() { // Developer > Stats addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats); + // Developer > Advanced Settings... + action = addActionToQMenuAndActionHash(developerMenu, "Advanced Preferences..."); + connect(action, &QAction::triggered, [] { + qApp->showDialog(QString("hifi/dialogs/AdvancedPreferencesDialog.qml"), + QString("hifi/tablet/AdvancedPreferencesDialog.qml"), "AdvancedPreferencesDialog"); + }); + // Developer > API Debugger action = addActionToQMenuAndActionHash(developerMenu, "API Debugger"); connect(action, &QAction::triggered, [] { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 11a27ff7f5..8cb1804fd4 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -46,9 +46,11 @@ namespace MenuOption { const QString AutoMuteAudio = "Auto Mute Microphone"; const QString AvatarReceiveStats = "Show Receive Stats"; const QString AvatarBookmarks = "Avatar Bookmarks"; + const QString AvatarEntitiesBookmarks = "Avatar Entities Bookmarks"; const QString Back = "Back"; const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkAvatar = "Bookmark Avatar"; + const QString BookmarkAvatarEntities = "Bookmark Avatar Entities"; const QString BookmarkLocation = "Bookmark Location"; const QString CalibrateCamera = "Calibrate Camera"; const QString CameraEntityMode = "Entity Mode"; @@ -78,6 +80,7 @@ namespace MenuOption { const QString DecreaseAvatarSize = "Decrease Avatar Size"; const QString DefaultSkybox = "Default Skybox"; const QString DeleteAvatarBookmark = "Delete Avatar Bookmark..."; + const QString DeleteAvatarEntitiesBookmark = "Delete Avatar Entities Bookmark"; const QString DeleteBookmark = "Delete Bookmark..."; const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 93caef555f..b71c060465 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -546,7 +546,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic continue; } - QString extraInfo; + QVariantMap extraInfo; intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, distance, face, surfaceNormal, extraInfo, true); @@ -554,6 +554,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic result.intersects = true; result.avatarID = avatar->getID(); result.distance = distance; + result.extraInfo = extraInfo; } } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 2e633986a0..19a2d39a05 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include #include @@ -561,6 +563,12 @@ void MyAvatar::simulate(float deltaTime) { if (!_skeletonModel->getHeadPosition(headPosition)) { headPosition = getWorldPosition(); } + + if (isNaN(headPosition)) { + qCDebug(interfaceapp) << "MyAvatar::simulate headPosition is NaN"; + headPosition = glm::vec3(0.0f); + } + head->setPosition(headPosition); head->setScale(getModelScale()); head->simulate(deltaTime); @@ -1415,6 +1423,37 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { } +void MyAvatar::removeAvatarEntities() { + auto treeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + entityTree->withWriteLock([&] { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + entityTree->deleteEntity(entityID, true, true); + } + }); + } +} + +QVariantList MyAvatar::getAvatarEntitiesVariant() { + QVariantList avatarEntitiesData; + QScriptEngine scriptEngine; + forEachChild([&](SpatiallyNestablePointer child) { + if (child->getNestableType() == NestableType::Entity) { + auto modelEntity = std::dynamic_pointer_cast(child); + if (modelEntity) { + QVariantMap avatarEntityData; + EntityItemProperties entityProperties = modelEntity->getProperties(); + QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties); + avatarEntityData["properties"] = scriptProperties.toVariant(); + avatarEntitiesData.append(QVariant(avatarEntityData)); + } + } + }); + return avatarEntitiesData; +} + void MyAvatar::resetFullAvatarURL() { auto lastAvatarURL = getFullAvatarURLFromPreferences(); @@ -2414,7 +2453,6 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette }; auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, EntityItemID& entityIdOut, glm::vec3& normalOut) { OctreeElementPointer element; - EntityItemPointer intersectedEntity = NULL; float distance; BoxFace face; const bool visibleOnly = false; @@ -2426,13 +2464,14 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette const auto lockType = Octree::Lock; // Should we refactor to take a lock just once? bool* accurateResult = NULL; - bool intersects = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, - element, distance, face, normalOut, (void**)&intersectedEntity, lockType, accurateResult); - if (!intersects || !intersectedEntity) { + QVariantMap extraInfo; + EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, + element, distance, face, normalOut, extraInfo, lockType, accurateResult); + if (entityID.isNull()) { return false; } intersectionOut = startPointIn + (directionIn * distance); - entityIdOut = intersectedEntity->getEntityItemID(); + entityIdOut = entityID; return true; }; @@ -2700,27 +2739,48 @@ void MyAvatar::setWalkSpeed(float value) { } glm::vec3 MyAvatar::getPositionForAudio() { + glm::vec3 result; switch (_audioListenerMode) { case AudioListenerMode::FROM_HEAD: - return getHead()->getPosition(); + result = getHead()->getPosition(); + break; case AudioListenerMode::FROM_CAMERA: - return qApp->getCamera().getPosition(); + result = qApp->getCamera().getPosition(); + break; case AudioListenerMode::CUSTOM: - return _customListenPosition; + result = _customListenPosition; + break; } - return vec3(); + + if (isNaN(result)) { + qCDebug(interfaceapp) << "MyAvatar::getPositionForAudio produced NaN" << _audioListenerMode; + result = glm::vec3(0.0f); + } + + return result; } glm::quat MyAvatar::getOrientationForAudio() { + glm::quat result; + switch (_audioListenerMode) { case AudioListenerMode::FROM_HEAD: - return getHead()->getFinalOrientationInWorldFrame(); + result = getHead()->getFinalOrientationInWorldFrame(); + break; case AudioListenerMode::FROM_CAMERA: - return qApp->getCamera().getOrientation(); + result = qApp->getCamera().getOrientation(); + break; case AudioListenerMode::CUSTOM: - return _customListenOrientation; + result = _customListenOrientation; + break; } - return quat(); + + if (isNaN(result)) { + qCDebug(interfaceapp) << "MyAvatar::getOrientationForAudio produced NaN" << _audioListenerMode; + result = glm::quat(); + } + + return result; } void MyAvatar::setAudioListenerMode(AudioListenerMode audioListenerMode) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 58b49b61ff..8587ddc9c2 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -512,6 +512,9 @@ public: bool hasDriveInput() const; + QVariantList getAvatarEntitiesVariant(); + void removeAvatarEntities(); + Q_INVOKABLE bool isFlying(); Q_INVOKABLE bool isInAir(); Q_INVOKABLE void setFlyingEnabled(bool enabled); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 8b73042ada..9599af827f 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -591,8 +591,8 @@ void Wallet::chooseSecurityImage(const QString& filename) { if (_securityImage) { delete _securityImage; } - QString path = qApp->applicationDirPath(); - path.append("/resources/qml/hifi/commerce/wallet/"); + QString path = PathUtils::resourcesPath(); + path.append("/qml/hifi/commerce/wallet/"); path.append(filename); // now create a new security image pixmap diff --git a/interface/src/raypick/PickScriptingInterface.h b/interface/src/raypick/PickScriptingInterface.h index 0e0b3e113b..98427e34ca 100644 --- a/interface/src/raypick/PickScriptingInterface.h +++ b/interface/src/raypick/PickScriptingInterface.h @@ -114,6 +114,7 @@ public: * @property {float} distance The distance to the intersection point from the origin of the ray. * @property {Vec3} intersection The intersection point in world-space. * @property {Vec3} surfaceNormal The surface normal at the intersected point. All NANs if type == INTERSECTED_HUD. + * @property {Variant} extraInfo Additional intersection details when available for Model objects. * @property {PickRay} searchRay The PickRay that was used. Valid even if there was no intersection. */ @@ -127,6 +128,7 @@ public: * @property {float} distance The distance to the intersection point from the origin of the ray. * @property {Vec3} intersection The intersection point in world-space. * @property {Vec3} surfaceNormal The surface normal at the intersected point. All NANs if type == INTERSECTED_HUD. + * @property {Variant} extraInfo Additional intersection details when available for Model objects. * @property {StylusTip} stylusTip The StylusTip that was used. Valid even if there was no intersection. */ diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index 4a39d5df59..75b5e77fd8 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -19,7 +19,7 @@ PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) { DependencyManager::get()->findRayIntersectionVector(pick, !getFilter().doesPickCoarse(), getIncludeItemsAs(), getIgnoreItemsAs(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); if (entityRes.intersects) { - return std::make_shared(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, pick, entityRes.surfaceNormal); + return std::make_shared(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, pick, entityRes.surfaceNormal, entityRes.extraInfo); } else { return std::make_shared(pick.toVariantMap()); } @@ -30,7 +30,7 @@ PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { qApp->getOverlays().findRayIntersectionVector(pick, !getFilter().doesPickCoarse(), getIncludeItemsAs(), getIgnoreItemsAs(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); if (overlayRes.intersects) { - return std::make_shared(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, pick, overlayRes.surfaceNormal); + return std::make_shared(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, pick, overlayRes.surfaceNormal, overlayRes.extraInfo); } else { return std::make_shared(pick.toVariantMap()); } @@ -39,7 +39,7 @@ PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { PickResultPointer RayPick::getAvatarIntersection(const PickRay& pick) { RayToAvatarIntersectionResult avatarRes = DependencyManager::get()->findRayIntersectionVector(pick, getIncludeItemsAs(), getIgnoreItemsAs()); if (avatarRes.intersects) { - return std::make_shared(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, pick); + return std::make_shared(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, pick, glm::vec3(NAN), avatarRes.extraInfo); } else { return std::make_shared(pick.toVariantMap()); } diff --git a/interface/src/raypick/RayPick.h b/interface/src/raypick/RayPick.h index 25ad4df1f3..6bdc2cb5b0 100644 --- a/interface/src/raypick/RayPick.h +++ b/interface/src/raypick/RayPick.h @@ -18,8 +18,8 @@ class RayPickResult : public PickResult { public: RayPickResult() {} RayPickResult(const QVariantMap& pickVariant) : PickResult(pickVariant) {} - RayPickResult(const IntersectionType type, const QUuid& objectID, float distance, const glm::vec3& intersection, const PickRay& searchRay, const glm::vec3& surfaceNormal = glm::vec3(NAN)) : - PickResult(searchRay.toVariantMap()), type(type), intersects(type != NONE), objectID(objectID), distance(distance), intersection(intersection), surfaceNormal(surfaceNormal) { + RayPickResult(const IntersectionType type, const QUuid& objectID, float distance, const glm::vec3& intersection, const PickRay& searchRay, const glm::vec3& surfaceNormal = glm::vec3(NAN), const QVariantMap& extraInfo = QVariantMap()) : + PickResult(searchRay.toVariantMap()), type(type), intersects(type != NONE), objectID(objectID), distance(distance), intersection(intersection), surfaceNormal(surfaceNormal), extraInfo(extraInfo) { } RayPickResult(const RayPickResult& rayPickResult) : PickResult(rayPickResult.pickVariant) { @@ -29,6 +29,7 @@ public: distance = rayPickResult.distance; intersection = rayPickResult.intersection; surfaceNormal = rayPickResult.surfaceNormal; + extraInfo = rayPickResult.extraInfo; } IntersectionType type { NONE }; @@ -37,6 +38,7 @@ public: float distance { FLT_MAX }; glm::vec3 intersection { NAN }; glm::vec3 surfaceNormal { NAN }; + QVariantMap extraInfo; virtual QVariantMap toVariantMap() const override { QVariantMap toReturn; @@ -47,6 +49,7 @@ public: toReturn["intersection"] = vec3toVariant(intersection); toReturn["surfaceNormal"] = vec3toVariant(surfaceNormal); toReturn["searchRay"] = PickResult::toVariantMap(); + toReturn["extraInfo"] = extraInfo; return toReturn; } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index c13b5b2e0d..dc227c21ba 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -260,7 +260,7 @@ void AssetMappingModel::refresh() { for (auto& mapping : mappings) { auto& path = mapping.first; - if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + if (path.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { // Hide baked mappings continue; } @@ -303,7 +303,7 @@ void AssetMappingModel::refresh() { auto statusString = isFolder ? "--" : bakingStatusToString(mapping.second.status); lastItem->setData(statusString, Qt::UserRole + 5); lastItem->setData(mapping.second.bakingErrors, Qt::UserRole + 6); - if (mapping.second.status == Pending) { + if (mapping.second.status == AssetUtils::Pending) { ++numPendingBakes; } } diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index 6830de6e35..969f9895de 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -39,7 +39,6 @@ BaseLogDialog::BaseLogDialog(QWidget* parent) : QDialog(parent, Qt::Window) { QFile styleSheet(PathUtils::resourcesPath() + "styles/log_dialog.qss"); if (styleSheet.open(QIODevice::ReadOnly)) { - QDir::setCurrent(PathUtils::resourcesPath()); setStyleSheet(styleSheet.readAll()); } diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 1a09af07ab..48b56c7ced 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -82,19 +82,42 @@ void setupPreferences() { preference->setMax(500); preferences->addPreference(preference); } - { - auto getter = []()->float { return qApp->getDesktopTabletScale(); }; - auto setter = [](float value) { qApp->setDesktopTabletScale(value); }; - auto preference = new SpinnerPreference(UI_CATEGORY, "Desktop Tablet Scale %", getter, setter); - preference->setMin(20); - preference->setMax(500); - preferences->addPreference(preference); - } + + { auto getter = []()->bool { return qApp->getPreferStylusOverLaser(); }; auto setter = [](bool value) { qApp->setPreferStylusOverLaser(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Stylus Over Laser", getter, setter)); } + + static const QString ADVANCED_UI_CATEGORY { "Advanced UI" }; + { + auto getter = []()->float { return qApp->getDesktopTabletScale(); }; + auto setter = [](float value) { qApp->setDesktopTabletScale(value); }; + auto preference = new SpinnerPreference(ADVANCED_UI_CATEGORY, "Desktop Tablet Scale %", getter, setter); + preference->setMin(20); + preference->setMax(500); + preferences->addPreference(preference); + } + { + auto getter = [=]()->float { return myAvatar->getRealWorldFieldOfView(); }; + auto setter = [=](float value) { myAvatar->setRealWorldFieldOfView(value); }; + auto preference = new SpinnerPreference(ADVANCED_UI_CATEGORY, "Real world vertical field of view (angular size of monitor)", getter, setter); + preference->setMin(1); + preference->setMax(180); + preferences->addPreference(preference); + } + { + auto getter = []()->float { return qApp->getFieldOfView(); }; + auto setter = [](float value) { qApp->setFieldOfView(value); }; + auto preference = new SpinnerPreference(ADVANCED_UI_CATEGORY, "Vertical field of view", getter, setter); + preference->setMin(1); + preference->setMax(180); + preference->setStep(1); + preferences->addPreference(preference); + } + + // FIXME: Remove setting completely or make available through JavaScript API? /* { @@ -128,21 +151,13 @@ void setupPreferences() { preferences->addPreference(preference); } - // Scripts - { - auto getter = []()->QString { return DependencyManager::get()->getScriptsLocation(); }; - auto setter = [](const QString& value) { DependencyManager::get()->setScriptsLocation(value); }; - preferences->addPreference(new BrowsePreference("Scripts", "Load scripts from this directory", getter, setter)); - } - - preferences->addPreference(new ButtonPreference("Scripts", "Load Default Scripts", [] { - DependencyManager::get()->loadDefaultScripts(); - })); - { auto getter = []()->bool { return !Menu::getInstance()->isOptionChecked(MenuOption::DisableActivityLogger); }; auto setter = [](bool value) { Menu::getInstance()->setIsOptionChecked(MenuOption::DisableActivityLogger, !value); }; - preferences->addPreference(new CheckPreference("Privacy", "Send data", getter, setter)); + preferences->addPreference(new CheckPreference("Privacy", "Send data - High Fidelity uses information provided by your " + "client to improve the product through the logging of errors, tracking of usage patterns, " + "installation and system details, and crash events. By allowing High Fidelity to collect " + "this information you are helping to improve the product. ", getter, setter)); } static const QString LOD_TUNING("Level of Detail Tuning"); @@ -167,23 +182,6 @@ void setupPreferences() { } static const QString AVATAR_TUNING { "Avatar Tuning" }; - { - auto getter = [=]()->float { return myAvatar->getRealWorldFieldOfView(); }; - auto setter = [=](float value) { myAvatar->setRealWorldFieldOfView(value); }; - auto preference = new SpinnerPreference(AVATAR_TUNING, "Real world vertical field of view (angular size of monitor)", getter, setter); - preference->setMin(1); - preference->setMax(180); - preferences->addPreference(preference); - } - { - auto getter = []()->float { return qApp->getFieldOfView(); }; - auto setter = [](float value) { qApp->setFieldOfView(value); }; - auto preference = new SpinnerPreference(AVATAR_TUNING, "Vertical field of view", getter, setter); - preference->setMin(1); - preference->setMax(180); - preference->setStep(1); - preferences->addPreference(preference); - } { auto getter = [=]()->QString { return myAvatar->getDominantHand(); }; auto setter = [=](const QString& value) { myAvatar->setDominantHand(value); }; @@ -297,26 +295,6 @@ void setupPreferences() { } #endif - { - auto getter = []()->float { return qApp->getMaxOctreePacketsPerSecond(); }; - auto setter = [](float value) { qApp->setMaxOctreePacketsPerSecond(value); }; - auto preference = new SpinnerPreference("Octree", "Max packets sent each second", getter, setter); - preference->setMin(60); - preference->setMax(6000); - preference->setStep(10); - preferences->addPreference(preference); - } - - - { - auto getter = []()->float { return qApp->getApplicationCompositor().getHmdUIAngularSize(); }; - auto setter = [](float value) { qApp->getApplicationCompositor().setHmdUIAngularSize(value); }; - auto preference = new SpinnerPreference("HMD", "UI horizontal angular size (degrees)", getter, setter); - preference->setMin(30); - preference->setMax(160); - preference->setStep(1); - preferences->addPreference(preference); - } { static const QString RENDER("Graphics"); @@ -342,7 +320,7 @@ void setupPreferences() { } } { - static const QString RENDER("Networking"); + static const QString NETWORKING("Networking"); auto nodelist = DependencyManager::get(); { @@ -350,10 +328,21 @@ void setupPreferences() { static const int MAX_PORT_NUMBER { 65535 }; auto getter = [nodelist] { return static_cast(nodelist->getSocketLocalPort()); }; auto setter = [nodelist](int preset) { nodelist->setSocketLocalPort(static_cast(preset)); }; - auto preference = new IntSpinnerPreference(RENDER, "Listening Port", getter, setter); + auto preference = new IntSpinnerPreference(NETWORKING, "Listening Port", getter, setter); preference->setMin(MIN_PORT_NUMBER); preference->setMax(MAX_PORT_NUMBER); preferences->addPreference(preference); } + + { + auto getter = []()->float { return qApp->getMaxOctreePacketsPerSecond(); }; + auto setter = [](float value) { qApp->setMaxOctreePacketsPerSecond(value); }; + auto preference = new SpinnerPreference(NETWORKING, "Max entities packets sent each second", getter, setter); + preference->setMin(60); + preference->setMax(6000); + preference->setStep(10); + preferences->addPreference(preference); + } + } } diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index d65b9850d7..df0f3c4728 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -68,7 +68,7 @@ public: BoxFace& face, glm::vec3& surfaceNormal); virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo) { return findRayIntersection(origin, direction, distance, face, surfaceNormal); } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index d690880f99..d4138941ae 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -168,7 +168,7 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& _contextOverlay->setColorPulse(CONTEXT_OVERLAY_UNHOVERED_COLORPULSE); _contextOverlay->setIgnoreRayIntersection(false); _contextOverlay->setDrawInFront(true); - _contextOverlay->setURL(PathUtils::resourcesPath() + "images/inspect-icon.png"); + _contextOverlay->setURL(PathUtils::resourcesUrl() + "images/inspect-icon.png"); _contextOverlay->setIsFacingAvatar(true); _contextOverlayID = qApp->getOverlays().addOverlay(_contextOverlay); } diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 4804a2db3d..310dbf78d8 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -446,12 +446,12 @@ QVariant ModelOverlay::getProperty(const QString& property) { bool ModelOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) { - QString subMeshNameTemp; - return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, subMeshNameTemp); + QVariantMap extraInfo; + return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, extraInfo); } bool ModelOverlay::findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo) { return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, extraInfo); } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index a39f762210..60ba90e568 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -41,7 +41,7 @@ public: virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) override; + float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo) override; virtual ModelOverlay* createClone() const override; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 7f897aee4a..6898b5ed2b 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -520,7 +520,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionVector(const PickRay float thisDistance; BoxFace thisFace; glm::vec3 thisSurfaceNormal; - QString thisExtraInfo; + QVariantMap thisExtraInfo; if (thisOverlay->findRayIntersectionExtraInfo(ray.origin, ray.direction, thisDistance, thisFace, thisSurfaceNormal, thisExtraInfo)) { bool isDrawInFront = thisOverlay->getDrawInFront(); @@ -578,7 +578,7 @@ QScriptValue RayToOverlayIntersectionResultToScriptValue(QScriptEngine* engine, obj.setProperty("face", faceName); auto intersection = vec3toScriptValue(engine, value.intersection); obj.setProperty("intersection", intersection); - obj.setProperty("extraInfo", value.extraInfo); + obj.setProperty("extraInfo", engine->toScriptValue(value.extraInfo)); return obj; } @@ -612,7 +612,7 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& objectVar value.intersection = newIntersection; } } - value.extraInfo = object["extraInfo"].toString(); + value.extraInfo = object["extraInfo"].toMap(); } bool Overlays::isLoaded(OverlayID id) { diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index fd57869048..113ee92b80 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -51,6 +51,7 @@ const OverlayID UNKNOWN_OVERLAY_ID = OverlayID(); * @property {number} distance - The distance from the {@link PickRay} origin to the intersection point. * @property {Vec3} surfaceNormal - The normal of the overlay surface at the intersection point. * @property {Vec3} intersection - The position of the intersection point. + * @property {Object} extraInfo Additional intersection details, if available. */ class RayToOverlayIntersectionResult { public: @@ -60,7 +61,7 @@ public: BoxFace face; glm::vec3 surfaceNormal; glm::vec3 intersection; - QString extraInfo; + QVariantMap extraInfo; }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b1b41775a8..309bb59cff 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -445,22 +445,31 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo } bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { + bool success { false }; if (QThread::currentThread() == thread()) { if (isIndexValid(jointIndex)) { position = (rotation * _internalPoseSet._absolutePoses[jointIndex].trans()) + translation; - return true; + success = true; } else { - return false; + success = false; + } + } else { + QReadLocker readLock(&_externalPoseSetLock); + if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._absolutePoses.size()) { + position = (rotation * _externalPoseSet._absolutePoses[jointIndex].trans()) + translation; + success = true; + } else { + success = false; } } - QReadLocker readLock(&_externalPoseSetLock); - if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._absolutePoses.size()) { - position = (rotation * _externalPoseSet._absolutePoses[jointIndex].trans()) + translation; - return true; - } else { - return false; + if (isNaN(position)) { + qCWarning(animation) << "Rig::getJointPositionInWorldFrame produces NaN"; + success = false; + position = glm::vec3(0.0f); } + + return success; } bool Rig::getJointPosition(int jointIndex, glm::vec3& position) const { diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 9f32372a8e..d1b919292a 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -427,28 +427,11 @@ AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const fl options.stereo = sound->isStereo(); options.position = position; options.volume = volume; + options.pitch = 1.0f / stretchFactor; QByteArray samples = sound->getByteArray(); - if (stretchFactor == 1.0f) { - return playSoundAndDelete(samples, options); - } - const int standardRate = AudioConstants::SAMPLE_RATE; - const int resampledRate = standardRate * stretchFactor; - const int channelCount = sound->isStereo() ? 2 : 1; - - AudioSRC resampler(standardRate, resampledRate, channelCount); - - const int nInputFrames = samples.size() / (channelCount * sizeof(int16_t)); - const int maxOutputFrames = resampler.getMaxOutput(nInputFrames); - QByteArray resampled(maxOutputFrames * channelCount * sizeof(int16_t), '\0'); - - int nOutputFrames = resampler.render(reinterpret_cast(samples.data()), - reinterpret_cast(resampled.data()), - nInputFrames); - - Q_UNUSED(nOutputFrames); - return playSoundAndDelete(resampled, options); + return playSoundAndDelete(samples, options); } AudioInjectorPointer AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { @@ -461,12 +444,40 @@ AudioInjectorPointer AudioInjector::playSoundAndDelete(const QByteArray& buffer, return sound; } - AudioInjectorPointer AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjectorPointer injector = AudioInjectorPointer::create(buffer, options); - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread injector"; + if (options.pitch == 1.0f) { + + AudioInjectorPointer injector = AudioInjectorPointer::create(buffer, options); + + if (!injector->inject(&AudioInjectorManager::threadInjector)) { + qWarning() << "AudioInjector::playSound failed to thread injector"; + } + return injector; + + } else { + + const int standardRate = AudioConstants::SAMPLE_RATE; + const int resampledRate = AudioConstants::SAMPLE_RATE / glm::clamp(options.pitch, 1/16.0f, 16.0f); // limit to 4 octaves + const int numChannels = options.ambisonic ? AudioConstants::AMBISONIC : + (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int nInputFrames = buffer.size() / (numChannels * sizeof(int16_t)); + const int maxOutputFrames = resampler.getMaxOutput(nInputFrames); + QByteArray resampledBuffer(maxOutputFrames * numChannels * sizeof(int16_t), '\0'); + + resampler.render(reinterpret_cast(buffer.data()), + reinterpret_cast(resampledBuffer.data()), + nInputFrames); + + AudioInjectorPointer injector = AudioInjectorPointer::create(resampledBuffer, options); + + if (!injector->inject(&AudioInjectorManager::threadInjector)) { + qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; + } + return injector; } - return injector; } diff --git a/libraries/audio/src/AudioInjectorOptions.cpp b/libraries/audio/src/AudioInjectorOptions.cpp index 0af74a796c..2f82cae137 100644 --- a/libraries/audio/src/AudioInjectorOptions.cpp +++ b/libraries/audio/src/AudioInjectorOptions.cpp @@ -26,7 +26,8 @@ AudioInjectorOptions::AudioInjectorOptions() : ambisonic(false), ignorePenumbra(false), localOnly(false), - secondOffset(0.0f) + secondOffset(0.0f), + pitch(1.0f) { } @@ -40,6 +41,7 @@ QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInje obj.setProperty("ignorePenumbra", injectorOptions.ignorePenumbra); obj.setProperty("localOnly", injectorOptions.localOnly); obj.setProperty("secondOffset", injectorOptions.secondOffset); + obj.setProperty("pitch", injectorOptions.pitch); return obj; } @@ -87,6 +89,12 @@ void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOpt } else { qCWarning(audio) << "Audio injector options: secondOffset is not a number"; } + } else if (it.name() == "pitch") { + if (it.value().isNumber()) { + injectorOptions.pitch = it.value().toNumber(); + } else { + qCWarning(audio) << "Audio injector options: pitch is not a number"; + } } else { qCWarning(audio) << "Unknown audio injector option:" << it.name(); } diff --git a/libraries/audio/src/AudioInjectorOptions.h b/libraries/audio/src/AudioInjectorOptions.h index 40a3f343bd..4dd38ce915 100644 --- a/libraries/audio/src/AudioInjectorOptions.h +++ b/libraries/audio/src/AudioInjectorOptions.h @@ -29,6 +29,7 @@ public: bool ignorePenumbra; bool localOnly; float secondOffset; + float pitch; // multiplier, where 2.0f shifts up one octave }; Q_DECLARE_METATYPE(AudioInjectorOptions); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c2aadb8723..74888283df 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2504,6 +2504,7 @@ QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, c obj.setProperty("distance", value.distance); QScriptValue intersection = vec3toScriptValue(engine, value.intersection); obj.setProperty("intersection", intersection); + obj.setProperty("extraInfo", engine->toScriptValue(value.extraInfo)); return obj; } @@ -2516,6 +2517,7 @@ void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, Ra if (intersection.isValid()) { vec3FromScriptValue(intersection, value.intersection); } + value.extraInfo = object.property("extraInfo").toVariant().toMap(); } const float AvatarData::OUT_OF_VIEW_PENALTY = -10.0f; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 92c505f5c3..a363fb6d15 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -982,6 +982,7 @@ RayToAvatarIntersectionResult() : intersects(false), avatarID(), distance(0) {} QUuid avatarID; float distance; glm::vec3 intersection; + QVariantMap extraInfo; }; Q_DECLARE_METATYPE(RayToAvatarIntersectionResult) diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index f448375f0d..6c2acb7e62 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -53,8 +53,6 @@ public: bool calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction, glm::vec3& result) const; - float getHmdUIAngularSize() const { return _hmdUIAngularSize; } - void setHmdUIAngularSize(float hmdUIAngularSize) { _hmdUIAngularSize = hmdUIAngularSize; } bool isHMD() const; bool fakeEventActive() const { return _fakeMouseEvent; } @@ -139,7 +137,6 @@ private: //quint64 _hoverItemEnterUsecs { 0 }; bool _isOverDesktop { true }; - float _hmdUIAngularSize { glm::degrees(VIRTUAL_UI_TARGET_FOV.y) }; float _textureFov { VIRTUAL_UI_TARGET_FOV.y }; float _textureAspectRatio { VIRTUAL_UI_ASPECT_RATIO }; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c2a201a965..ba81922979 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -669,15 +669,16 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { - auto properties = rayPickResult.entity->getProperties(); + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { + auto properties = entity->getProperties(); QString urlString = properties.getHref(); QUrl url = QUrl(urlString, QUrl::StrictMode); if (url.isValid() && !url.isEmpty()){ DependencyManager::get()->handleLookupString(urlString); } - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Press, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -708,8 +709,9 @@ void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Press, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -738,10 +740,11 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { // qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID; - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Release, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -757,7 +760,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { // Even if we're no longer intersecting with an entity, if we started clicking on it, and now // we're releasing the button, then this is considered a clickReleaseOn event if (!_currentClickingOnEntityID.isInvalidID()) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Release, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -782,8 +785,9 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Move, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -797,7 +801,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { // if we were previously hovering over an entity, and this new entity is not the same as our previous entity // then we need to send the hover leave. if (!_currentHoverOverEntityID.isInvalidID() && rayPickResult.entityID != _currentHoverOverEntityID) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Move, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -828,7 +832,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { // if we were previously hovering over an entity, and we're no longer hovering over any entity then we need to // send the hover leave for our previous entity if (!_currentHoverOverEntityID.isInvalidID()) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Move, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 2e39e2e498..9fcb7640ef 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -282,7 +282,7 @@ bool RenderableModelEntityItem::supportsDetailedRayIntersection() const { bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, - glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const { + glm::vec3& surfaceNormal, QVariantMap& extraInfo, bool precisionPicking) const { auto model = getModel(); if (!model) { return true; @@ -290,9 +290,8 @@ bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& ori // qCDebug(entitiesrenderer) << "RenderableModelEntityItem::findDetailedRayIntersection() precisionPicking:" // << precisionPicking; - QString extraInfo; return model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, - face, surfaceNormal, extraInfo, precisionPicking, false); + face, surfaceNormal, extraInfo, precisionPicking, false); } void RenderableModelEntityItem::getCollisionGeometryResource() { @@ -1329,7 +1328,9 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce _currentTextures = newTextures; } } - + if (entity->_needsJointSimulation) { + entity->copyAnimationJointDataToModel(); + } entity->updateModelBounds(); entity->stopModelOverrideIfNoParent(); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index b3988e0239..33fc9910a0 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -70,7 +70,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void setShapeType(ShapeType type) override; virtual void setCompoundShapeURL(const QString& url) override; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 019ef0e414..6566486e57 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -567,7 +567,7 @@ public: bool RenderablePolyVoxEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const + QVariantMap& extraInfo, bool precisionPicking) const { // TODO -- correctly pick against marching-cube generated meshes if (!precisionPicking) { diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 03c7351a59..db0f0b729a 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -55,7 +55,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void setVoxelData(const QByteArray& voxelData) override; virtual void setVoxelVolumeSize(const glm::vec3& voxelVolumeSize) override; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index f9559a375b..4c398b8a29 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -160,7 +160,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { return true; } + QVariantMap& extraInfo, bool precisionPicking) const { return true; } // attributes applicable to all entity types EntityTypes::EntityType getType() const { return _type; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 206065beeb..4342f0e683 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -816,13 +816,12 @@ RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionWorke RayToEntityIntersectionResult result; if (_entityTree) { OctreeElementPointer element; - EntityItemPointer intersectedEntity = NULL; - result.intersects = _entityTree->findRayIntersection(ray.origin, ray.direction, + result.entityID = _entityTree->findRayIntersection(ray.origin, ray.direction, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking, element, result.distance, result.face, result.surfaceNormal, - (void**)&intersectedEntity, lockType, &result.accurate); - if (result.intersects && intersectedEntity) { - result.entityID = intersectedEntity->getEntityItemID(); + result.extraInfo, lockType, &result.accurate); + result.intersects = !result.entityID.isNull(); + if (result.intersects) { result.intersection = ray.origin + (ray.direction * result.distance); } } @@ -988,8 +987,7 @@ RayToEntityIntersectionResult::RayToEntityIntersectionResult() : accurate(true), // assume it's accurate entityID(), distance(0), - face(), - entity(NULL) + face() { } @@ -1036,6 +1034,7 @@ QScriptValue RayToEntityIntersectionResultToScriptValue(QScriptEngine* engine, c QScriptValue surfaceNormal = vec3toScriptValue(engine, value.surfaceNormal); obj.setProperty("surfaceNormal", surfaceNormal); + obj.setProperty("extraInfo", engine->toScriptValue(value.extraInfo)); return obj; } @@ -1071,6 +1070,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra if (surfaceNormal.isValid()) { vec3FromScriptValue(surfaceNormal, value.surfaceNormal); } + value.extraInfo = object.property("extraInfo").toVariant().toMap(); } bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 095a821482..4c4e2ffbfd 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -62,7 +62,7 @@ public: BoxFace face; glm::vec3 intersection; glm::vec3 surfaceNormal; - EntityItemPointer entity; + QVariantMap extraInfo; }; Q_DECLARE_METATYPE(RayToEntityIntersectionResult) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 15fc3256bd..765cdda6eb 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -59,8 +59,8 @@ public: float& distance; BoxFace& face; glm::vec3& surfaceNormal; - void** intersectedObject; - bool found; + QVariantMap& extraInfo; + EntityItemID entityID; }; @@ -748,23 +748,24 @@ bool findRayIntersectionOp(const OctreeElementPointer& element, void* extraData) RayArgs* args = static_cast(extraData); bool keepSearching = true; EntityTreeElementPointer entityTreeElementPointer = std::static_pointer_cast(element); - if (entityTreeElementPointer->findRayIntersection(args->origin, args->direction, keepSearching, + EntityItemID entityID = entityTreeElementPointer->findRayIntersection(args->origin, args->direction, keepSearching, args->element, args->distance, args->face, args->surfaceNormal, args->entityIdsToInclude, - args->entityIdsToDiscard, args->visibleOnly, args->collidableOnly, args->intersectedObject, args->precisionPicking)) { - args->found = true; + args->entityIdsToDiscard, args->visibleOnly, args->collidableOnly, args->extraInfo, args->precisionPicking); + if (!entityID.isNull()) { + args->entityID = entityID; } return keepSearching; } -bool EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, +EntityItemID EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, bool visibleOnly, bool collidableOnly, bool precisionPicking, OctreeElementPointer& element, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, + BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, Octree::lockType lockType, bool* accurateResult) { RayArgs args = { origin, direction, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking, - element, distance, face, surfaceNormal, intersectedObject, false }; + element, distance, face, surfaceNormal, extraInfo, EntityItemID() }; distance = FLT_MAX; bool requireLock = lockType == Octree::Lock; @@ -776,7 +777,7 @@ bool EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& d *accurateResult = lockResult; // if user asked to accuracy or result, let them know this is accurate } - return args.found; + return args.entityID; } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 11a747d624..8cb89d6493 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -97,11 +97,11 @@ public: virtual void processChallengeOwnershipReplyPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) override; virtual void processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) override; - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + virtual EntityItemID findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, bool visibleOnly, bool collidableOnly, bool precisionPicking, OctreeElementPointer& node, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject = NULL, + BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, Octree::lockType lockType = Octree::TryLock, bool* accurateResult = NULL); virtual bool rootElementHasData() const override { return true; } diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 926975f735..c9e1ecc3f1 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -588,57 +588,60 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3 return false; } -bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, +EntityItemID EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly, - void** intersectedObject, bool precisionPicking) { + QVariantMap& extraInfo, bool precisionPicking) { keepSearching = true; // assume that we will continue searching after this. + EntityItemID result; float distanceToElementCube = std::numeric_limits::max(); float distanceToElementDetails = distance; BoxFace localFace; glm::vec3 localSurfaceNormal; + QVariantMap localExtraInfo; // if the ray doesn't intersect with our cube, we can stop searching! if (!_cube.findRayIntersection(origin, direction, distanceToElementCube, localFace, localSurfaceNormal)) { keepSearching = false; // no point in continuing to search - return false; // we did not intersect + return result; // we did not intersect } // by default, we only allow intersections with leaves with content if (!canRayIntersect()) { - return false; // we don't intersect with non-leaves, and we keep searching + return result; // we don't intersect with non-leaves, and we keep searching } // if the distance to the element cube is not less than the current best distance, then it's not possible // for any details inside the cube to be closer so we don't need to consider them. if (_cube.contains(origin) || distanceToElementCube < distance) { - if (findDetailedRayIntersection(origin, direction, keepSearching, element, distanceToElementDetails, + EntityItemID entityID = findDetailedRayIntersection(origin, direction, keepSearching, element, distanceToElementDetails, face, localSurfaceNormal, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, - intersectedObject, precisionPicking, distanceToElementCube)) { - + localExtraInfo, precisionPicking, distanceToElementCube); + if (!entityID.isNull()) { if (distanceToElementDetails < distance) { distance = distanceToElementDetails; face = localFace; surfaceNormal = localSurfaceNormal; - return true; + extraInfo = localExtraInfo; + result = entityID; } } } - return false; + return result; } -bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, +EntityItemID EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIDsToDiscard, - bool visibleOnly, bool collidableOnly, void** intersectedObject, bool precisionPicking, float distanceToElementCube) { + bool visibleOnly, bool collidableOnly, QVariantMap& extraInfo, bool precisionPicking, float distanceToElementCube) { // only called if we do intersect our bounding cube, but find if we actually intersect with entities... int entityNumber = 0; - bool somethingIntersected = false; + EntityItemID entityID; forEachEntity([&](EntityItemPointer entity) { if ( (visibleOnly && !entity->isVisible()) || (collidableOnly && (entity->getCollisionless() || entity->getShapeType() == SHAPE_TYPE_NONE)) || (entityIdsToInclude.size() > 0 && !entityIdsToInclude.contains(entity->getID())) @@ -655,6 +658,7 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con float localDistance; BoxFace localFace; glm::vec3 localSurfaceNormal; + QVariantMap localExtraInfo; // if the ray doesn't intersect with our cube, we can stop searching! if (!entityBox.findRayIntersection(origin, direction, localDistance, localFace, localSurfaceNormal)) { @@ -684,14 +688,14 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con // now ask the entity if we actually intersect if (entity->supportsDetailedRayIntersection()) { if (entity->findDetailedRayIntersection(origin, direction, keepSearching, element, localDistance, - localFace, localSurfaceNormal, intersectedObject, precisionPicking)) { + localFace, localSurfaceNormal, localExtraInfo, precisionPicking)) { if (localDistance < distance) { distance = localDistance; face = localFace; surfaceNormal = localSurfaceNormal; - *intersectedObject = (void*)entity.get(); - somethingIntersected = true; + extraInfo = localExtraInfo; + entityID = entity->getEntityItemID(); } } } else { @@ -701,15 +705,14 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con distance = localDistance; face = localFace; surfaceNormal = glm::vec3(rotation * glm::vec4(localSurfaceNormal, 1.0f)); - *intersectedObject = (void*)entity.get(); - somethingIntersected = true; + entityID = entity->getEntityItemID(); } } } } entityNumber++; }); - return somethingIntersected; + return entityID; } // TODO: change this to use better bounding shape for entity than sphere diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index cafae9941a..a524904c71 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -146,16 +146,16 @@ public: virtual bool deleteApproved() const override { return !hasEntities(); } virtual bool canRayIntersect() const override { return hasEntities(); } - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + virtual EntityItemID findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& node, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, - const QVector& entityIdsToDiscard, bool visibleOnly = false, bool collidableOnly = false, - void** intersectedObject = NULL, bool precisionPicking = false); - virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly, + QVariantMap& extraInfo, bool precisionPicking = false); + virtual EntityItemID findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly, - void** intersectedObject, bool precisionPicking, float distanceToElementCube); + QVariantMap& extraInfo, bool precisionPicking, float distanceToElementCube); virtual bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration, void** penetratedObject) const override; diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index 45f36a1744..85edefa413 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -300,7 +300,7 @@ void LightEntityItem::resetLightPropertiesChanged() { bool LightEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { // TODO: consider if this is really what we want to do. We've made it so that "lights are pickable" is a global state // this is probably reasonable since there's typically only one tree you'd be picking on at a time. Technically we could diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h index 870b283c26..3be1d48aa5 100644 --- a/libraries/entities/src/LightEntityItem.h +++ b/libraries/entities/src/LightEntityItem.h @@ -88,7 +88,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; private: // properties of a light diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 7fda113510..9f16807084 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -63,7 +63,7 @@ class LineEntityItem : public EntityItem { virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, + QVariantMap& extraInfo, bool precisionPicking) const override { return false; } bool pointsChanged() const { return _pointsChanged; } void resetPointsChanged(); diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 3164a9646b..8af2b26216 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -96,7 +96,7 @@ class PolyLineEntityItem : public EntityItem { virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override { return false; } + QVariantMap& extraInfo, bool precisionPicking) const override { return false; } // disable these external interfaces as PolyLineEntities caculate their own dimensions based on the points they contain virtual void setRegistrationPoint(const glm::vec3& value) override {}; // FIXME: this is suspicious! diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index d8d998b944..47d2a4b4e1 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -47,7 +47,7 @@ class PolyVoxEntityItem : public EntityItem { virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override { return false; } + QVariantMap& extraInfo, bool precisionPicking) const override { return false; } virtual void debugDump() const override; diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 3750bc3b57..cbcfcaaa1d 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -223,7 +223,7 @@ bool ShapeEntityItem::supportsDetailedRayIntersection() const { bool ShapeEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { // determine the ray in the frame of the entity transformed from a unit sphere glm::mat4 entityToWorldMatrix = getEntityToWorldMatrix(); glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index c5df17db54..84ce1ce57e 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -94,7 +94,7 @@ public: bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; void debugDump() const override; diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index 639da69a17..7b1089e6ed 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -131,7 +131,7 @@ void TextEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits bool TextEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { glm::vec3 dimensions = getScaledDimensions(); glm::vec2 xyDimensions(dimensions.x, dimensions.y); glm::quat rotation = getWorldOrientation(); diff --git a/libraries/entities/src/TextEntityItem.h b/libraries/entities/src/TextEntityItem.h index 520a935e55..3ab743ecfd 100644 --- a/libraries/entities/src/TextEntityItem.h +++ b/libraries/entities/src/TextEntityItem.h @@ -50,7 +50,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; static const QString DEFAULT_TEXT; void setText(const QString& value); diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 54db6e7c3b..91e7bca063 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -108,7 +108,7 @@ void WebEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitst bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { glm::vec3 dimensions = getScaledDimensions(); glm::vec2 xyDimensions(dimensions.x, dimensions.y); glm::quat rotation = getWorldOrientation(); diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index f5066beebc..7d8f37cd83 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -49,7 +49,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void setSourceUrl(const QString& value); QString getSourceUrl() const; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 107e7cf561..6083e5b8de 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -298,7 +298,7 @@ void ZoneEntityItem::setCompoundShapeURL(const QString& url) { bool ZoneEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { return _zonesArePickable; } diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 05dcc05416..95b6248fde 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -107,7 +107,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void debugDump() const override; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 4bc5eb1b6d..4fac3b8f0f 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1733,8 +1733,18 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS qCDebug(modelformat) << "Joint not in model list: " << jointID; fbxCluster.jointIndex = 0; } + fbxCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform; + + // slam bottom row to (0, 0, 0, 1), we KNOW this is not a perspective matrix and + // sometimes floating point fuzz can be introduced after the inverse. + fbxCluster.inverseBindMatrix[0][3] = 0.0f; + fbxCluster.inverseBindMatrix[1][3] = 0.0f; + fbxCluster.inverseBindMatrix[2][3] = 0.0f; + fbxCluster.inverseBindMatrix[3][3] = 1.0f; + fbxCluster.inverseBindTransform = Transform(fbxCluster.inverseBindMatrix); + extracted.mesh.clusters.append(fbxCluster); // override the bind rotation with the transform link @@ -1836,13 +1846,13 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } // now that we've accumulated the most relevant weights for each vertex - // normalize and compress to 8-bits + // normalize and compress to 16-bits extracted.mesh.clusterWeights.fill(0, numClusterIndices); int numVertices = extracted.mesh.vertices.size(); for (int i = 0; i < numVertices; ++i) { int j = i * WEIGHTS_PER_VERTEX; - // normalize weights into uint8_t + // normalize weights into uint16_t float totalWeight = weightAccumulators[j]; for (int k = j + 1; k < j + WEIGHTS_PER_VERTEX; ++k) { totalWeight += weightAccumulators[k]; @@ -1881,6 +1891,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.meshes.append(extracted.mesh); int meshIndex = geometry.meshes.size() - 1; + if (extracted.mesh._mesh) { + extracted.mesh._mesh->displayName = QString("%1#/mesh/%2").arg(url).arg(meshIndex); + } meshIDsToMeshIndices.insert(it.key(), meshIndex); } @@ -1949,7 +1962,19 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } } - + { + int i = 0; + for (const auto& mesh : geometry.meshes) { + auto name = geometry.getModelNameOfMesh(i++); + if (!name.isEmpty()) { + if (mesh._mesh) { + mesh._mesh->displayName += "#" + name; + } else { + qDebug() << "modelName but no mesh._mesh" << name; + } + } + } + } return geometryPtr; } @@ -1965,7 +1990,7 @@ FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri reader._loadLightmaps = loadLightmaps; reader._lightmapLevel = lightmapLevel; - qDebug() << "Reading FBX: " << url; + qCDebug(modelformat) << "Reading FBX: " << url; return reader.extractFBXGeometry(mapping, url); } diff --git a/libraries/graphics/src/graphics/Geometry.h b/libraries/graphics/src/graphics/Geometry.h index cd64a7c185..642aa9e38d 100755 --- a/libraries/graphics/src/graphics/Geometry.h +++ b/libraries/graphics/src/graphics/Geometry.h @@ -135,6 +135,8 @@ public: static MeshPointer createIndexedTriangles_P3F(uint32_t numVertices, uint32_t numTriangles, const glm::vec3* vertices = nullptr, const uint32_t* indices = nullptr); + QString displayName; + protected: gpu::Stream::FormatPointer _vertexFormat; diff --git a/libraries/graphics/src/graphics/Haze.h b/libraries/graphics/src/graphics/Haze.h index 608449a97e..eda9f1e407 100644 --- a/libraries/graphics/src/graphics/Haze.h +++ b/libraries/graphics/src/graphics/Haze.h @@ -117,13 +117,14 @@ namespace graphics { // Amount of background (skybox) to display, overriding the haze effect for the background float hazeBackgroundBlend{ INITIAL_HAZE_BACKGROUND_BLEND }; - // The haze attenuation exponents used by both fragment and directional light attenuation float hazeRangeFactor{ convertHazeRangeToHazeRangeFactor(INITIAL_HAZE_RANGE) }; float hazeHeightFactor{ convertHazeAltitudeToHazeAltitudeFactor(INITIAL_HAZE_HEIGHT) }; - float hazeKeyLightRangeFactor{ convertHazeRangeToHazeRangeFactor(INITIAL_KEY_LIGHT_RANGE) }; + float hazeKeyLightAltitudeFactor{ convertHazeAltitudeToHazeAltitudeFactor(INITIAL_KEY_LIGHT_ALTITUDE) }; + // Padding required to align the structure to sizeof(vec4) + vec3 __padding; Parameters() {} }; diff --git a/libraries/graphics/src/graphics/Light.slh b/libraries/graphics/src/graphics/Light.slh index 6b24f89c3c..e1202ed6a0 100644 --- a/libraries/graphics/src/graphics/Light.slh +++ b/libraries/graphics/src/graphics/Light.slh @@ -56,10 +56,10 @@ Light getLight(int index) { } <@else@> -uniform lightBuffer { +uniform keyLightBuffer { Light light; }; -Light getLight() { +Light getKeyLight() { return light; } diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 6fdfc5a42b..1db93e8cb9 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -20,6 +20,7 @@ #include #include +#include #include "AssetRequest.h" #include "AssetUpload.h" @@ -72,7 +73,177 @@ void AssetClient::init() { networkAccessManager.setCache(cache); qInfo() << "ResourceManager disk cache setup at" << _cacheDir << "(size:" << MAXIMUM_CACHE_SIZE / BYTES_PER_GIGABYTES << "GB)"; + } else { + auto cache = qobject_cast(networkAccessManager.cache()); + qInfo() << "ResourceManager disk cache already setup at" << cache->cacheDirectory() + << "(size:" << cache->maximumCacheSize() / BYTES_PER_GIGABYTES << "GB)"; } + +} + +namespace { + const QString& CACHE_ERROR_MESSAGE{ "AssetClient::Error: %1 %2" }; +} + +MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise deferred) { + if (!deferred) { + deferred = makePromise(__FUNCTION__); // create on caller's thread + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "cacheInfoRequestAsync", Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = qobject_cast(NetworkAccessManager::getInstance().cache()); + if (cache) { + deferred->resolve({ + { "cacheDirectory", cache->cacheDirectory() }, + { "cacheSize", cache->cacheSize() }, + { "maximumCacheSize", cache->maximumCacheSize() }, + }); + } else { + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg("cache unavailable")); + } + } + return deferred; +} + +MiniPromise::Promise AssetClient::queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "queryCacheMetaAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = NetworkAccessManager::getInstance().cache(); + if (cache) { + QNetworkCacheMetaData metaData = cache->metaData(url); + QVariantMap attributes, rawHeaders; + if (!metaData.isValid()) { + deferred->reject("invalid cache entry", { + { "_url", url }, + { "isValid", metaData.isValid() }, + { "metaDataURL", metaData.url() }, + }); + } else { + auto metaAttributes = metaData.attributes(); + foreach(QNetworkRequest::Attribute k, metaAttributes.keys()) { + attributes[QString::number(k)] = metaAttributes[k]; + } + for (const auto& i : metaData.rawHeaders()) { + rawHeaders[i.first] = i.second; + } + deferred->resolve({ + { "_url", url }, + { "isValid", metaData.isValid() }, + { "url", metaData.url() }, + { "expirationDate", metaData.expirationDate() }, + { "lastModified", metaData.lastModified().toString().isEmpty() ? QDateTime() : metaData.lastModified() }, + { "saveToDisk", metaData.saveToDisk() }, + { "attributes", attributes }, + { "rawHeaders", rawHeaders }, + }); + } + } else { + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg("cache unavailable")); + } + } + return deferred; +} + +MiniPromise::Promise AssetClient::loadFromCacheAsync(const QUrl& url, MiniPromise::Promise deferred) { + auto errorMessage = CACHE_ERROR_MESSAGE.arg(__FUNCTION__); + if (!deferred) { + deferred = makePromise(__FUNCTION__); // create on caller's thread + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadFromCacheAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = NetworkAccessManager::getInstance().cache(); + if (cache) { + MiniPromise::Promise metaRequest = makePromise(__FUNCTION__); + queryCacheMetaAsync(url, metaRequest); + metaRequest->finally([&](QString error, QVariantMap metadata) { + QVariantMap result = { + { "url", url }, + { "metadata", metadata }, + { "data", QByteArray() }, + }; + if (!error.isEmpty()) { + deferred->reject(error, result); + return; + } + // caller is responsible for the deletion of the ioDevice, hence the unique_ptr + auto ioDevice = std::unique_ptr(cache->data(url)); + if (ioDevice) { + result["data"] = ioDevice->readAll(); + } else { + error = errorMessage.arg("error reading data"); + } + deferred->handle(error, result); + }); + } else { + deferred->reject(errorMessage.arg("cache unavailable")); + } + } + return deferred; +} + +namespace { + // parse RFC 1123 HTTP date format + QDateTime parseHttpDate(const QString& dateString) { + QDateTime dt = QDateTime::fromString(dateString.left(25), "ddd, dd MMM yyyy HH:mm:ss"); + if (!dt.isValid()) { + dt = QDateTime::fromString(dateString, Qt::ISODateWithMs); + } + if (!dt.isValid()) { + qDebug() << __FUNCTION__ << "unrecognized date format:" << dateString; + } + dt.setTimeSpec(Qt::UTC); + return dt; + } + QDateTime getHttpDateValue(const QVariantMap& headers, const QString& keyName, const QDateTime& defaultValue) { + return headers.contains(keyName) ? parseHttpDate(headers[keyName].toString()) : defaultValue; + } +} + +MiniPromise::Promise AssetClient::saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& headers, MiniPromise::Promise deferred) { + if (!deferred) { + deferred = makePromise(__FUNCTION__); // create on caller's thread + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod( + this, "saveToCacheAsync", Qt::QueuedConnection, + Q_ARG(const QUrl&, url), + Q_ARG(const QByteArray&, data), + Q_ARG(const QVariantMap&, headers), + Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = NetworkAccessManager::getInstance().cache(); + if (cache) { + QNetworkCacheMetaData metaData; + metaData.setUrl(url); + metaData.setSaveToDisk(true); + metaData.setLastModified(getHttpDateValue(headers, "last-modified", QDateTime::currentDateTimeUtc())); + metaData.setExpirationDate(getHttpDateValue(headers, "expires", QDateTime())); // nil defaultValue == never expires + auto ioDevice = cache->prepare(metaData); + if (ioDevice) { + ioDevice->write(data); + cache->insert(ioDevice); + qCDebug(asset_client) << url.toDisplayString() << "saved to disk cache ("<< data.size()<<" bytes)"; + deferred->resolve({ + { "url", url }, + { "success", true }, + { "metaDataURL", metaData.url() }, + { "byteLength", data.size() }, + { "expirationDate", metaData.expirationDate() }, + { "lastModified", metaData.lastModified().toString().isEmpty() ? QDateTime() : metaData.lastModified() }, + }); + } else { + auto error = QString("Could not save %1 to disk cache").arg(url.toDisplayString()); + qCWarning(asset_client) << error; + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg(error)); + } + } else { + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg("unavailable")); + } + } + return deferred; } void AssetClient::cacheInfoRequest(QObject* reciever, QString slot) { @@ -113,7 +284,7 @@ void AssetClient::handleAssetMappingOperationReply(QSharedPointerreadPrimitive(&messageID); - AssetServerError error; + AssetUtils::AssetServerError error; message->readPrimitive(&error); // Check if we have any pending requests for this node @@ -149,7 +320,7 @@ bool haveAssetServer() { return true; } -GetMappingRequest* AssetClient::createGetMappingRequest(const AssetPath& path) { +GetMappingRequest* AssetClient::createGetMappingRequest(const AssetUtils::AssetPath& path) { auto request = new GetMappingRequest(path); request->moveToThread(thread()); @@ -165,7 +336,7 @@ GetAllMappingsRequest* AssetClient::createGetAllMappingsRequest() { return request; } -DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetPathList& paths) { +DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetUtils::AssetPathList& paths) { auto request = new DeleteMappingsRequest(paths); request->moveToThread(thread()); @@ -173,7 +344,7 @@ DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetPathL return request; } -SetMappingRequest* AssetClient::createSetMappingRequest(const AssetPath& path, const AssetHash& hash) { +SetMappingRequest* AssetClient::createSetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) { auto request = new SetMappingRequest(path, hash); request->moveToThread(thread()); @@ -181,7 +352,7 @@ SetMappingRequest* AssetClient::createSetMappingRequest(const AssetPath& path, c return request; } -RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) { +RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath) { auto request = new RenameMappingRequest(oldPath, newPath); request->moveToThread(thread()); @@ -189,7 +360,7 @@ RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& o return request; } -SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetPathList& path, bool enabled) { +SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetUtils::AssetPathList& path, bool enabled) { auto bakingEnabledRequest = new SetBakingEnabledRequest(path, enabled); bakingEnabledRequest->moveToThread(thread()); @@ -197,7 +368,7 @@ SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetP return bakingEnabledRequest; } -AssetRequest* AssetClient::createRequest(const AssetHash& hash, const ByteRange& byteRange) { +AssetRequest* AssetClient::createRequest(const AssetUtils::AssetHash& hash, const ByteRange& byteRange) { auto request = new AssetRequest(hash, byteRange); // Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case) @@ -222,11 +393,11 @@ AssetUpload* AssetClient::createUpload(const QByteArray& data) { return upload; } -MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffset end, +MessageID AssetClient::getAsset(const QString& hash, AssetUtils::DataOffset start, AssetUtils::DataOffset end, ReceivedAssetCallback callback, ProgressCallback progressCallback) { Q_ASSERT(QThread::currentThread() == thread()); - if (hash.length() != SHA256_HASH_HEX_LENGTH) { + if (hash.length() != AssetUtils::SHA256_HASH_HEX_LENGTH) { qCWarning(asset_client) << "Invalid hash size"; return false; } @@ -238,7 +409,7 @@ MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffse auto messageID = ++_currentID; - auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(start) + sizeof(end); + auto payloadSize = sizeof(messageID) + AssetUtils::SHA256_HASH_LENGTH + sizeof(start) + sizeof(end); auto packet = NLPacket::create(PacketType::AssetGet, payloadSize, true); qCDebug(asset_client) << "Requesting data from" << start << "to" << end << "of" << hash << "from asset-server."; @@ -257,7 +428,7 @@ MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffse } } - callback(false, AssetServerError::NoError, QByteArray()); + callback(false, AssetUtils::AssetServerError::NoError, QByteArray()); return INVALID_MESSAGE_ID; } @@ -270,7 +441,7 @@ MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callbac if (assetServer) { auto messageID = ++_currentID; - auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH; + auto payloadSize = sizeof(messageID) + AssetUtils::SHA256_HASH_LENGTH; auto packet = NLPacket::create(PacketType::AssetGetInfo, payloadSize, true); packet->writePrimitive(messageID); @@ -283,7 +454,7 @@ MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callbac } } - callback(false, AssetServerError::NoError, { "", 0 }); + callback(false, AssetUtils::AssetServerError::NoError, { "", 0 }); return INVALID_MESSAGE_ID; } @@ -292,14 +463,14 @@ void AssetClient::handleAssetGetInfoReply(QSharedPointer messag MessageID messageID; message->readPrimitive(&messageID); - auto assetHash = message->read(SHA256_HASH_LENGTH); + auto assetHash = message->read(AssetUtils::SHA256_HASH_LENGTH); - AssetServerError error; + AssetUtils::AssetServerError error; message->readPrimitive(&error); AssetInfo info { assetHash.toHex(), 0 }; - if (error == AssetServerError::NoError) { + if (error == AssetUtils::AssetServerError::NoError) { message->readPrimitive(&info.size); } @@ -326,16 +497,16 @@ void AssetClient::handleAssetGetInfoReply(QSharedPointer messag void AssetClient::handleAssetGetReply(QSharedPointer message, SharedNodePointer senderNode) { Q_ASSERT(QThread::currentThread() == thread()); - auto assetHash = message->readHead(SHA256_HASH_LENGTH); + auto assetHash = message->readHead(AssetUtils::SHA256_HASH_LENGTH); qCDebug(asset_client) << "Got reply for asset: " << assetHash.toHex(); MessageID messageID; message->readHeadPrimitive(&messageID); - AssetServerError error; + AssetUtils::AssetServerError error; message->readHeadPrimitive(&error); - DataOffset length = 0; + AssetUtils::DataOffset length = 0; if (!error) { message->readHeadPrimitive(&length); } else { @@ -388,7 +559,7 @@ void AssetClient::handleAssetGetReply(QSharedPointer message, S } void AssetClient::handleProgressCallback(const QWeakPointer& node, MessageID messageID, - qint64 size, DataOffset length) { + qint64 size, AssetUtils::DataOffset length) { auto senderNode = node.toStrongRef(); if (!senderNode) { @@ -414,7 +585,7 @@ void AssetClient::handleProgressCallback(const QWeakPointer& node, Message callbacks.progressCallback(size, length); } -void AssetClient::handleCompleteCallback(const QWeakPointer& node, MessageID messageID, DataOffset length) { +void AssetClient::handleCompleteCallback(const QWeakPointer& node, MessageID messageID, AssetUtils::DataOffset length) { auto senderNode = node.toStrongRef(); if (!senderNode) { @@ -448,9 +619,9 @@ void AssetClient::handleCompleteCallback(const QWeakPointer& node, Message } if (message->failed() || length != message->getBytesLeftToRead()) { - callbacks.completeCallback(false, AssetServerError::NoError, QByteArray()); + callbacks.completeCallback(false, AssetUtils::AssetServerError::NoError, QByteArray()); } else { - callbacks.completeCallback(true, AssetServerError::NoError, message->readAll()); + callbacks.completeCallback(true, AssetUtils::AssetServerError::NoError, message->readAll()); } // We should never get to this point without the associated senderNode and messageID @@ -461,7 +632,7 @@ void AssetClient::handleCompleteCallback(const QWeakPointer& node, Message } -MessageID AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCallback callback) { +MessageID AssetClient::getAssetMapping(const AssetUtils::AssetPath& path, MappingOperationCallback callback) { Q_ASSERT(QThread::currentThread() == thread()); auto nodeList = DependencyManager::get(); @@ -473,7 +644,7 @@ MessageID AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCa auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Get); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Get); packetList->writeString(path); @@ -484,7 +655,7 @@ MessageID AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCa } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } @@ -500,7 +671,7 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::GetAll); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::GetAll); if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { _pendingMappingRequests[assetServer][messageID] = callback; @@ -509,11 +680,11 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback) { +MessageID AssetClient::deleteAssetMappings(const AssetUtils::AssetPathList& paths, MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); @@ -523,7 +694,7 @@ MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOp auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Delete); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Delete); packetList->writePrimitive(int(paths.size())); @@ -538,11 +709,11 @@ MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOp } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback) { +MessageID AssetClient::setAssetMapping(const QString& path, const AssetUtils::AssetHash& hash, MappingOperationCallback callback) { Q_ASSERT(QThread::currentThread() == thread()); auto nodeList = DependencyManager::get(); @@ -554,7 +725,7 @@ MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& has auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Set); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Set); packetList->writeString(path); packetList->write(QByteArray::fromHex(hash.toUtf8())); @@ -566,11 +737,11 @@ MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& has } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback) { +MessageID AssetClient::renameAssetMapping(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath, MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); @@ -580,7 +751,7 @@ MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetP auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Rename); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Rename); packetList->writeString(oldPath); packetList->writeString(newPath); @@ -593,11 +764,11 @@ MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetP } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback) { +MessageID AssetClient::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled, MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); @@ -607,7 +778,7 @@ MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::SetBakingEnabled); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::SetBakingEnabled); packetList->writePrimitive(enabled); @@ -624,7 +795,7 @@ MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } @@ -708,7 +879,7 @@ MessageID AssetClient::uploadAsset(const QByteArray& data, UploadResultCallback } } - callback(false, AssetServerError::NoError, QString()); + callback(false, AssetUtils::AssetServerError::NoError, QString()); return INVALID_MESSAGE_ID; } @@ -718,7 +889,7 @@ void AssetClient::handleAssetUploadReply(QSharedPointer message MessageID messageID; message->readPrimitive(&messageID); - AssetServerError error; + AssetUtils::AssetServerError error; message->readPrimitive(&error); QString hashString; @@ -726,7 +897,7 @@ void AssetClient::handleAssetUploadReply(QSharedPointer message if (error) { qCWarning(asset_client) << "Error uploading file to asset server"; } else { - auto hash = message->read(SHA256_HASH_LENGTH); + auto hash = message->read(AssetUtils::SHA256_HASH_LENGTH); hashString = hash.toHex(); qCDebug(asset_client) << "Successfully uploaded asset to asset-server - SHA256 hash is " << hashString; @@ -765,7 +936,7 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { auto messageMapIt = _pendingUploads.find(node); if (messageMapIt != _pendingUploads.end()) { for (const auto& value : messageMapIt->second) { - value.second(false, AssetServerError::NoError, ""); + value.second(false, AssetUtils::AssetServerError::NoError, ""); } messageMapIt->second.clear(); } @@ -797,7 +968,7 @@ void AssetClient::forceFailureOfPendingRequests(SharedNodePointer node) { disconnect(message.data(), nullptr, this, nullptr); } - value.second.completeCallback(false, AssetServerError::NoError, QByteArray()); + value.second.completeCallback(false, AssetUtils::AssetServerError::NoError, QByteArray()); } messageMapIt->second.clear(); } @@ -808,7 +979,7 @@ void AssetClient::forceFailureOfPendingRequests(SharedNodePointer node) { if (messageMapIt != _pendingInfoRequests.end()) { AssetInfo info { "", 0 }; for (const auto& value : messageMapIt->second) { - value.second(false, AssetServerError::NoError, info); + value.second(false, AssetUtils::AssetServerError::NoError, info); } messageMapIt->second.clear(); } @@ -818,7 +989,7 @@ void AssetClient::forceFailureOfPendingRequests(SharedNodePointer node) { auto messageMapIt = _pendingMappingRequests.find(node); if (messageMapIt != _pendingMappingRequests.end()) { for (const auto& value : messageMapIt->second) { - value.second(false, AssetServerError::NoError, QSharedPointer()); + value.second(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); } messageMapIt->second.clear(); } diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 8035aa886e..3ec96c3dd4 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -19,6 +19,7 @@ #include #include +#include #include "AssetUtils.h" #include "ByteRange.h" @@ -41,10 +42,10 @@ struct AssetInfo { int64_t size; }; -using MappingOperationCallback = std::function message)>; -using ReceivedAssetCallback = std::function; -using GetInfoCallback = std::function; -using UploadResultCallback = std::function; +using MappingOperationCallback = std::function message)>; +using ReceivedAssetCallback = std::function; +using GetInfoCallback = std::function; +using UploadResultCallback = std::function; using ProgressCallback = std::function; class AssetClient : public QObject, public Dependency { @@ -52,13 +53,13 @@ class AssetClient : public QObject, public Dependency { public: AssetClient(); - Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetPath& path); + Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetUtils::AssetPath& path); Q_INVOKABLE GetAllMappingsRequest* createGetAllMappingsRequest(); - Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths); - Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash); - Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath); - Q_INVOKABLE SetBakingEnabledRequest* createSetBakingEnabledRequest(const AssetPathList& path, bool enabled); - Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash, const ByteRange& byteRange = ByteRange()); + Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetUtils::AssetPathList& paths); + Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); + Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath); + Q_INVOKABLE SetBakingEnabledRequest* createSetBakingEnabledRequest(const AssetUtils::AssetPathList& path, bool enabled); + Q_INVOKABLE AssetRequest* createRequest(const AssetUtils::AssetHash& hash, const ByteRange& byteRange = ByteRange()); Q_INVOKABLE AssetUpload* createUpload(const QString& filename); Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data); @@ -66,6 +67,10 @@ public slots: void init(); void cacheInfoRequest(QObject* reciever, QString slot); + MiniPromise::Promise cacheInfoRequestAsync(MiniPromise::Promise deferred = nullptr); + MiniPromise::Promise queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred = nullptr); + MiniPromise::Promise loadFromCacheAsync(const QUrl& url, MiniPromise::Promise deferred = nullptr); + MiniPromise::Promise saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& metadata = QVariantMap(), MiniPromise::Promise deferred = nullptr); void clearCache(); private slots: @@ -78,15 +83,15 @@ private slots: void handleNodeClientConnectionReset(SharedNodePointer node); private: - MessageID getAssetMapping(const AssetHash& hash, MappingOperationCallback callback); + MessageID getAssetMapping(const AssetUtils::AssetHash& hash, MappingOperationCallback callback); MessageID getAllAssetMappings(MappingOperationCallback callback); - MessageID setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback); - MessageID deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback); - MessageID renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback); - MessageID setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback); + MessageID setAssetMapping(const QString& path, const AssetUtils::AssetHash& hash, MappingOperationCallback callback); + MessageID deleteAssetMappings(const AssetUtils::AssetPathList& paths, MappingOperationCallback callback); + MessageID renameAssetMapping(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath, MappingOperationCallback callback); + MessageID setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled, MappingOperationCallback callback); MessageID getAssetInfo(const QString& hash, GetInfoCallback callback); - MessageID getAsset(const QString& hash, DataOffset start, DataOffset end, + MessageID getAsset(const QString& hash, AssetUtils::DataOffset start, AssetUtils::DataOffset end, ReceivedAssetCallback callback, ProgressCallback progressCallback); MessageID uploadAsset(const QByteArray& data, UploadResultCallback callback); @@ -95,8 +100,8 @@ private: bool cancelGetAssetRequest(MessageID id); bool cancelUploadAssetRequest(MessageID id); - void handleProgressCallback(const QWeakPointer& node, MessageID messageID, qint64 size, DataOffset length); - void handleCompleteCallback(const QWeakPointer& node, MessageID messageID, DataOffset length); + void handleProgressCallback(const QWeakPointer& node, MessageID messageID, qint64 size, AssetUtils::DataOffset length); + void handleCompleteCallback(const QWeakPointer& node, MessageID messageID, AssetUtils::DataOffset length); void forceFailureOfPendingRequests(SharedNodePointer node); diff --git a/libraries/networking/src/AssetRequest.cpp b/libraries/networking/src/AssetRequest.cpp index 7fa563d4ad..c42e9aff14 100644 --- a/libraries/networking/src/AssetRequest.cpp +++ b/libraries/networking/src/AssetRequest.cpp @@ -52,7 +52,7 @@ void AssetRequest::start() { } // in case we haven't parsed a valid hash, return an error now - if (!isValidHash(_hash)) { + if (!AssetUtils::isValidHash(_hash)) { _error = InvalidHash; _state = Finished; @@ -61,7 +61,7 @@ void AssetRequest::start() { } // Try to load from cache - _data = loadFromCache(getUrl()); + _data = AssetUtils::loadFromCache(getUrl()); if (!_data.isNull()) { _error = NoError; @@ -80,7 +80,7 @@ void AssetRequest::start() { auto hash = _hash; _assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive, - [this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) { + [this, that, hash](bool responseReceived, AssetUtils::AssetServerError serverError, const QByteArray& data) { if (!that) { qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error; @@ -91,12 +91,12 @@ void AssetRequest::start() { if (!responseReceived) { _error = NetworkError; - } else if (serverError != AssetServerError::NoError) { + } else if (serverError != AssetUtils::AssetServerError::NoError) { switch (serverError) { - case AssetServerError::AssetNotFound: + case AssetUtils::AssetServerError::AssetNotFound: _error = NotFound; break; - case AssetServerError::InvalidByteRange: + case AssetUtils::AssetServerError::InvalidByteRange: _error = InvalidByteRange; break; default: @@ -104,7 +104,7 @@ void AssetRequest::start() { break; } } else { - if (!_byteRange.isSet() && hashData(data).toHex() != _hash) { + if (!_byteRange.isSet() && AssetUtils::hashData(data).toHex() != _hash) { // the hash of the received data does not match what we expect, so we return an error _error = HashVerificationFailed; } @@ -115,7 +115,7 @@ void AssetRequest::start() { emit progress(_totalReceived, data.size()); if (!_byteRange.isSet()) { - saveToCache(getUrl(), data); + AssetUtils::saveToCache(getUrl(), data); } } } @@ -134,3 +134,14 @@ void AssetRequest::start() { emit progress(totalReceived, total); }); } + + +const QString AssetRequest::getErrorString() const { + QString result; + if (_error != Error::NoError) { + QVariant v; + v.setValue(_error); + result = v.toString(); // courtesy of Q_ENUM + } + return result; +} diff --git a/libraries/networking/src/AssetRequest.h b/libraries/networking/src/AssetRequest.h index a7213a90d7..bf901c4e8f 100644 --- a/libraries/networking/src/AssetRequest.h +++ b/libraries/networking/src/AssetRequest.h @@ -42,7 +42,7 @@ public: NetworkError, UnknownError }; - + Q_ENUM(Error) AssetRequest(const QString& hash, const ByteRange& byteRange = ByteRange()); virtual ~AssetRequest() override; @@ -51,7 +51,8 @@ public: const QByteArray& getData() const { return _data; } const State& getState() const { return _state; } const Error& getError() const { return _error; } - QUrl getUrl() const { return ::getATPUrl(_hash); } + const QString getErrorString() const; + QUrl getUrl() const { return AssetUtils::getATPUrl(_hash); } QString getHash() const { return _hash; } bool loadedFromCache() const { return _loadedFromCache; } diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index 55d0ad4f75..6d5bbb3ac5 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -68,7 +68,7 @@ void AssetResourceRequest::doSend() { } } -void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { +void AssetResourceRequest::requestMappingForPath(const AssetUtils::AssetPath& path) { auto statTracker = DependencyManager::get(); statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_STARTED); @@ -140,7 +140,7 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { _assetMappingRequest->start(); } -void AssetResourceRequest::requestHash(const AssetHash& hash) { +void AssetResourceRequest::requestHash(const AssetUtils::AssetHash& hash) { // Make request to atp auto assetClient = DependencyManager::get(); _assetRequest = assetClient->createRequest(hash, _byteRange); diff --git a/libraries/networking/src/AssetResourceRequest.h b/libraries/networking/src/AssetResourceRequest.h index ac36c83985..2fe79040ca 100644 --- a/libraries/networking/src/AssetResourceRequest.h +++ b/libraries/networking/src/AssetResourceRequest.h @@ -34,8 +34,8 @@ private slots: private: static bool urlIsAssetHash(const QUrl& url); - void requestMappingForPath(const AssetPath& path); - void requestHash(const AssetHash& hash); + void requestMappingForPath(const AssetUtils::AssetPath& path); + void requestHash(const AssetUtils::AssetHash& hash); GetMappingRequest* _assetMappingRequest { nullptr }; AssetRequest* _assetRequest { nullptr }; diff --git a/libraries/networking/src/AssetUpload.cpp b/libraries/networking/src/AssetUpload.cpp index 077a0388cf..f1c84e474a 100644 --- a/libraries/networking/src/AssetUpload.cpp +++ b/libraries/networking/src/AssetUpload.cpp @@ -81,21 +81,21 @@ void AssetUpload::start() { qCDebug(asset_client) << "Attempting to upload" << _filename << "to asset-server."; } - assetClient->uploadAsset(_data, [this](bool responseReceived, AssetServerError error, const QString& hash){ + assetClient->uploadAsset(_data, [this](bool responseReceived, AssetUtils::AssetServerError error, const QString& hash){ if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::AssetTooLarge: + case AssetUtils::AssetServerError::AssetTooLarge: _error = TooLarge; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; - case AssetServerError::FileOperationFailed: + case AssetUtils::AssetServerError::FileOperationFailed: _error = ServerFileError; break; default: @@ -104,8 +104,8 @@ void AssetUpload::start() { } } - if (_error == NoError && hash == hashData(_data).toHex()) { - saveToCache(getATPUrl(hash), _data); + if (_error == NoError && hash == AssetUtils::hashData(_data).toHex()) { + AssetUtils::saveToCache(AssetUtils::getATPUrl(hash), _data); } emit finished(this, hash); diff --git a/libraries/networking/src/AssetUtils.cpp b/libraries/networking/src/AssetUtils.cpp index 76fda6aed4..117274eab8 100644 --- a/libraries/networking/src/AssetUtils.cpp +++ b/libraries/networking/src/AssetUtils.cpp @@ -15,6 +15,7 @@ #include #include +#include // for baseName #include #include "NetworkAccessManager.h" @@ -22,8 +23,38 @@ #include "ResourceManager.h" -QUrl getATPUrl(const QString& hash) { - return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP, hash)); +namespace AssetUtils { + +// Extract the valid AssetHash portion from atp: URLs like "[atp:]HASH[.fbx][?query]" +// (or an invalid AssetHash if not found) +AssetHash extractAssetHash(const QString& input) { + if (isValidHash(input)) { + return input; + } + QString path = getATPUrl(input).path(); + QString baseName = QFileInfo(path).baseName(); + if (isValidHash(baseName)) { + return baseName; + } + return AssetHash(); +} + +// Get the normalized ATP URL for a raw hash, /path or "atp:" input string. +QUrl getATPUrl(const QString& input) { + QUrl url = input; + if (!url.scheme().isEmpty() && url.scheme() != URL_SCHEME_ATP) { + return QUrl(); + } + // this strips extraneous info from the URL (while preserving fragment/querystring) + QString path = url.toEncoded( + QUrl::RemoveAuthority | QUrl::RemoveScheme | + QUrl::StripTrailingSlash | QUrl::NormalizePathSegments + ); + QString baseName = QFileInfo(url.path()).baseName(); + if (isValidPath(path) || isValidHash(baseName)) { + return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP).arg(path)); + } + return QUrl(); } QByteArray hashData(const QByteArray& data) { @@ -102,3 +133,5 @@ QString bakingStatusToString(BakingStatus status) { return "--"; } } + +} // namespace AssetUtils diff --git a/libraries/networking/src/AssetUtils.h b/libraries/networking/src/AssetUtils.h index a7c053c3d6..42e8dfad62 100644 --- a/libraries/networking/src/AssetUtils.h +++ b/libraries/networking/src/AssetUtils.h @@ -19,6 +19,8 @@ #include #include +namespace AssetUtils { + using DataOffset = int64_t; using AssetPath = QString; @@ -71,7 +73,8 @@ struct MappingInfo { using AssetMapping = std::map; -QUrl getATPUrl(const QString& hash); +QUrl getATPUrl(const QString& input); +AssetHash extractAssetHash(const QString& input); QByteArray hashData(const QByteArray& data); @@ -84,4 +87,6 @@ bool isValidHash(const QString& hashString); QString bakingStatusToString(BakingStatus status); +} // namespace AssetUtils + #endif // hifi_AssetUtils_h diff --git a/libraries/networking/src/BaseAssetScriptingInterface.cpp b/libraries/networking/src/BaseAssetScriptingInterface.cpp new file mode 100644 index 0000000000..3149bbc768 --- /dev/null +++ b/libraries/networking/src/BaseAssetScriptingInterface.cpp @@ -0,0 +1,365 @@ +// +// BaseAssetScriptingInterface.cpp +// libraries/networking/src +// +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include "BaseAssetScriptingInterface.h" + +#include +#include +#include +#include + +#include "AssetRequest.h" +#include "AssetUpload.h" +#include "AssetUtils.h" +#include "MappingRequest.h" +#include "NetworkLogging.h" + +#include + +#include +#include "Gzip.h" + +using Promise = MiniPromise::Promise; + +QSharedPointer BaseAssetScriptingInterface::assetClient() { + auto client = DependencyManager::get(); + Q_ASSERT(client); + if (!client) { + qDebug() << "BaseAssetScriptingInterface::assetClient unavailable"; + } + return client; +} + +BaseAssetScriptingInterface::BaseAssetScriptingInterface(QObject* parent) : QObject(parent) {} + +bool BaseAssetScriptingInterface::initializeCache() { + if (!assetClient()) { + return false; // not yet possible to initialize the cache + } + if (_cacheReady) { + return true; // cache is ready + } + + // attempt to initialize the cache + QMetaObject::invokeMethod(assetClient().data(), "init"); + + Promise deferred = makePromise("BaseAssetScriptingInterface--queryCacheStatus"); + deferred->then([this](QVariantMap result) { + _cacheReady = !result.value("cacheDirectory").toString().isEmpty(); + }); + deferred->fail([](QString error) { + qDebug() << "BaseAssetScriptingInterface::queryCacheStatus ERROR" << QThread::currentThread() << error; + }); + assetClient()->cacheInfoRequestAsync(deferred); + return false; // cache is not ready yet +} + +Promise BaseAssetScriptingInterface::getCacheStatus() { + return assetClient()->cacheInfoRequestAsync(makePromise(__FUNCTION__)); +} + +Promise BaseAssetScriptingInterface::queryCacheMeta(const QUrl& url) { + return assetClient()->queryCacheMetaAsync(url, makePromise(__FUNCTION__)); +} + +Promise BaseAssetScriptingInterface::loadFromCache(const QUrl& url, bool decompress, const QString& responseType) { + QVariantMap metaData = { + { "_type", "cache" }, + { "url", url }, + { "responseType", responseType }, + }; + + Promise completed = makePromise("loadFromCache::completed"); + Promise fetched = makePromise("loadFromCache::fetched"); + + Promise downloaded = assetClient()->loadFromCacheAsync(url, makePromise("loadFromCache-retrieval")); + downloaded->mixin(metaData); + downloaded->fail(fetched); + + if (decompress) { + downloaded->then([=](QVariantMap result) { + fetched->mixin(result); + Promise decompressed = decompressBytes(result.value("data").toByteArray()); + decompressed->mixin(result); + decompressed->ready(fetched); + }); + } else { + downloaded->then(fetched); + } + + fetched->fail(completed); + fetched->then([=](QVariantMap result) { + Promise converted = convertBytes(result.value("data").toByteArray(), responseType); + converted->mixin(result); + converted->ready(completed); + }); + + return completed; +} + +Promise BaseAssetScriptingInterface::saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& headers) { + return assetClient()->saveToCacheAsync(url, data, headers, makePromise(__FUNCTION__)); +} + +Promise BaseAssetScriptingInterface::loadAsset(QString asset, bool decompress, QString responseType) { + auto hash = AssetUtils::extractAssetHash(asset); + auto url = AssetUtils::getATPUrl(hash).toString(); + + QVariantMap metaData = { + { "_asset", asset }, + { "_type", "download" }, + { "hash", hash }, + { "url", url }, + { "responseType", responseType }, + }; + + Promise completed = makePromise("loadAsset::completed"); + Promise fetched = makePromise("loadAsset::fetched"); + + Promise downloaded = downloadBytes(hash); + downloaded->mixin(metaData); + downloaded->fail(fetched); + + if (decompress) { + downloaded->then([=](QVariantMap result) { + Q_ASSERT(thread() == QThread::currentThread()); + fetched->mixin(result); + Promise decompressed = decompressBytes(result.value("data").toByteArray()); + decompressed->mixin(result); + decompressed->ready(fetched); + }); + } else { + downloaded->then(fetched); + } + + fetched->fail(completed); + fetched->then([=](QVariantMap result) { + Promise converted = convertBytes(result.value("data").toByteArray(), responseType); + converted->mixin(result); + converted->ready(completed); + }); + + return completed; +} + +Promise BaseAssetScriptingInterface::convertBytes(const QByteArray& dataByteArray, const QString& responseType) { + QVariantMap result = { + { "_contentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() }, + { "_byteLength", dataByteArray.size() }, + { "_responseType", responseType }, + }; + QString error; + Promise conversion = makePromise(__FUNCTION__); + if (!RESPONSE_TYPES.contains(responseType)) { + error = QString("convertBytes: invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | ")); + } else if (responseType == "arraybuffer") { + // interpret as bytes + result["response"] = dataByteArray; + } else if (responseType == "text") { + // interpret as utf-8 text + result["response"] = QString::fromUtf8(dataByteArray); + } else if (responseType == "json") { + // interpret as JSON + QJsonParseError status; + auto parsed = QJsonDocument::fromJson(dataByteArray, &status); + if (status.error == QJsonParseError::NoError) { + result["response"] = parsed.isArray() ? QVariant(parsed.array().toVariantList()) : QVariant(parsed.object().toVariantMap()); + } else { + result = { + { "error", status.error }, + { "offset", status.offset }, + }; + error = "JSON Parse Error: " + status.errorString(); + } + } + if (result.value("response").canConvert()) { + auto data = result.value("response").toByteArray(); + result["contentType"] = QMimeDatabase().mimeTypeForData(data).name(); + result["byteLength"] = data.size(); + result["responseType"] = responseType; + } + return conversion->handle(error, result); +} + +Promise BaseAssetScriptingInterface::decompressBytes(const QByteArray& dataByteArray) { + QByteArray inflated; + Promise decompressed = makePromise(__FUNCTION__); + auto start = usecTimestampNow(); + if (gunzip(dataByteArray, inflated)) { + auto end = usecTimestampNow(); + decompressed->resolve({ + { "_compressedByteLength", dataByteArray.size() }, + { "_compressedContentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() }, + { "_compressMS", (double)(end - start) / 1000.0 }, + { "decompressed", true }, + { "byteLength", inflated.size() }, + { "contentType", QMimeDatabase().mimeTypeForData(inflated).name() }, + { "data", inflated }, + }); + } else { + decompressed->reject("gunzip error"); + } + return decompressed; +} + +Promise BaseAssetScriptingInterface::compressBytes(const QByteArray& dataByteArray, int level) { + QByteArray deflated; + auto start = usecTimestampNow(); + Promise compressed = makePromise(__FUNCTION__); + if (gzip(dataByteArray, deflated, level)) { + auto end = usecTimestampNow(); + compressed->resolve({ + { "_uncompressedByteLength", dataByteArray.size() }, + { "_uncompressedContentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() }, + { "_compressMS", (double)(end - start) / 1000.0 }, + { "compressed", true }, + { "byteLength", deflated.size() }, + { "contentType", QMimeDatabase().mimeTypeForData(deflated).name() }, + { "data", deflated }, + }); + } else { + compressed->reject("gzip error", {}); + } + return compressed; +} + +Promise BaseAssetScriptingInterface::downloadBytes(QString hash) { + QPointer assetRequest = assetClient()->createRequest(hash); + Promise deferred = makePromise(__FUNCTION__); + + QObject::connect(assetRequest, &AssetRequest::finished, assetRequest, [this, deferred](AssetRequest* request) { + // note: we are now on the "Resource Manager" thread + Q_ASSERT(QThread::currentThread() == request->thread()); + Q_ASSERT(request->getState() == AssetRequest::Finished); + QString error; + QVariantMap result; + if (request->getError() == AssetRequest::Error::NoError) { + QByteArray data = request->getData(); + result = { + { "url", request->getUrl() }, + { "hash", request->getHash() }, + { "cached", request->loadedFromCache() }, + { "contentType", QMimeDatabase().mimeTypeForData(data).name() }, + { "data", data }, + }; + } else { + error = request->getError(); + result = { { "error", request->getError() } }; + } + // forward thread-safe copies back to our thread + deferred->handle(error, result); + request->deleteLater(); + }); + assetRequest->start(); + return deferred; +} + +Promise BaseAssetScriptingInterface::uploadBytes(const QByteArray& bytes) { + Promise deferred = makePromise(__FUNCTION__); + QPointer upload = assetClient()->createUpload(bytes); + + const auto byteLength = bytes.size(); + QObject::connect(upload, &AssetUpload::finished, upload, [=](AssetUpload* upload, const QString& hash) { + Q_ASSERT(QThread::currentThread() == upload->thread()); + // note: we are now on the "Resource Manager" thread + QString error; + QVariantMap result; + if (upload->getError() == AssetUpload::NoError) { + result = { + { "hash", hash }, + { "url", AssetUtils::getATPUrl(hash).toString() }, + { "filename", upload->getFilename() }, + { "byteLength", byteLength }, + }; + } else { + error = upload->getErrorString(); + result = { { "error", upload->getError() } }; + } + // forward thread-safe copies back to our thread + deferred->handle(error, result); + upload->deleteLater(); + }); + upload->start(); + return deferred; +} + +Promise BaseAssetScriptingInterface::getAssetInfo(QString asset) { + Promise deferred = makePromise(__FUNCTION__); + auto url = AssetUtils::getATPUrl(asset); + auto path = url.path(); + auto hash = AssetUtils::extractAssetHash(asset); + if (AssetUtils::isValidHash(hash)) { + // already a valid ATP hash -- nothing to do + deferred->resolve({ + { "hash", hash }, + { "path", path }, + { "url", url }, + }); + } else if (AssetUtils::isValidFilePath(path)) { + QPointer request = assetClient()->createGetMappingRequest(path); + + QObject::connect(request, &GetMappingRequest::finished, request, [=]() { + Q_ASSERT(QThread::currentThread() == request->thread()); + // note: we are now on the "Resource Manager" thread + QString error; + QVariantMap result; + if (request->getError() == GetMappingRequest::NoError) { + result = { + { "_hash", hash }, + { "_path", path }, + { "_url", url }, + { "url", url }, + { "hash", request->getHash() }, + { "hashURL", AssetUtils::getATPUrl(request->getHash()).toString() }, + { "wasRedirected", request->wasRedirected() }, + { "path", request->wasRedirected() ? request->getRedirectedPath() : path }, + }; + } else { + error = request->getErrorString(); + result = { { "error", request->getError() } }; + } + // forward thread-safe copies back to our thread + deferred->handle(error, result); + request->deleteLater(); + }); + request->start(); + } else { + deferred->reject("invalid ATP file path: " + asset + "("+path+")", {}); + } + return deferred; +} + +Promise BaseAssetScriptingInterface::symlinkAsset(QString hash, QString path) { + auto deferred = makePromise(__FUNCTION__); + QPointer setMappingRequest = assetClient()->createSetMappingRequest(path, hash); + + connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [=](SetMappingRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread + QString error; + QVariantMap result; + if (request->getError() == SetMappingRequest::NoError) { + result = { + { "_hash", hash }, + { "_path", path }, + { "hash", request->getHash() }, + { "path", request->getPath() }, + { "url", AssetUtils::getATPUrl(request->getPath()).toString() }, + }; + } else { + error = request->getErrorString(); + result = { { "error", request->getError() } }; + } + // forward results back to our thread + deferred->handle(error, result); + request->deleteLater(); + }); + setMappingRequest->start(); + return deferred; +} diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h new file mode 100644 index 0000000000..336f3f81db --- /dev/null +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -0,0 +1,59 @@ +// +// BaseAssetScriptingInterface.h +// libraries/networking/src +// +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// BaseAssetScriptingInterface contains the engine-agnostic support code that can be used from +// both QML JS and QScriptEngine JS engine implementations + +#ifndef hifi_BaseAssetScriptingInterface_h +#define hifi_BaseAssetScriptingInterface_h + +#include +#include +#include "AssetClient.h" +#include +#include "NetworkAccessManager.h" +#include + +class BaseAssetScriptingInterface : public QObject { + Q_OBJECT +public: + const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" }; + using Promise = MiniPromise::Promise; + QSharedPointer assetClient(); + + BaseAssetScriptingInterface(QObject* parent = nullptr); + +public slots: + bool isValidPath(QString input) { return AssetUtils::isValidPath(input); } + bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); } + QUrl getATPUrl(QString input) { return AssetUtils::getATPUrl(input); } + QString extractAssetHash(QString input) { return AssetUtils::extractAssetHash(input); } + bool isValidHash(QString input) { return AssetUtils::isValidHash(input); } + QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); } + QString hashDataHex(const QByteArray& data) { return hashData(data).toHex(); } + +protected: + bool _cacheReady{ false }; + bool initializeCache(); + Promise getCacheStatus(); + Promise queryCacheMeta(const QUrl& url); + Promise loadFromCache(const QUrl& url, bool decompress = false, const QString& responseType = "arraybuffer"); + Promise saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata = QVariantMap()); + + Promise loadAsset(QString asset, bool decompress, QString responseType); + Promise getAssetInfo(QString asset); + Promise downloadBytes(QString hash); + Promise uploadBytes(const QByteArray& bytes); + Promise compressBytes(const QByteArray& bytes, int level = -1); + Promise convertBytes(const QByteArray& dataByteArray, const QString& responseType); + Promise decompressBytes(const QByteArray& bytes); + Promise symlinkAsset(QString hash, QString path); +}; +#endif // hifi_BaseAssetScriptingInterface_h diff --git a/libraries/networking/src/MappingRequest.cpp b/libraries/networking/src/MappingRequest.cpp index a79105e3ab..07639d4994 100644 --- a/libraries/networking/src/MappingRequest.cpp +++ b/libraries/networking/src/MappingRequest.cpp @@ -51,13 +51,13 @@ QString MappingRequest::getErrorString() const { } } -GetMappingRequest::GetMappingRequest(const AssetPath& path) : _path(path.trimmed()) { +GetMappingRequest::GetMappingRequest(const AssetUtils::AssetPath& path) : _path(path.trimmed()) { }; void GetMappingRequest::doStart() { // short circuit the request if the path is invalid - if (!isValidFilePath(_path)) { + if (!AssetUtils::isValidFilePath(_path)) { _error = MappingRequest::InvalidPath; emit finished(this); return; @@ -66,17 +66,17 @@ void GetMappingRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->getAssetMapping(_path, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::AssetNotFound: + case AssetUtils::AssetServerError::AssetNotFound: _error = NotFound; break; default: @@ -86,7 +86,7 @@ void GetMappingRequest::doStart() { } if (!_error) { - _hash = message->read(SHA256_HASH_LENGTH).toHex(); + _hash = message->read(AssetUtils::SHA256_HASH_LENGTH).toHex(); // check the boolean to see if this request got re-directed quint8 wasRedirected; @@ -112,7 +112,7 @@ GetAllMappingsRequest::GetAllMappingsRequest() { void GetAllMappingsRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->getAllAssetMappings( - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; @@ -120,7 +120,7 @@ void GetAllMappingsRequest::doStart() { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; default: @@ -135,11 +135,11 @@ void GetAllMappingsRequest::doStart() { message->readPrimitive(&numberOfMappings); for (uint32_t i = 0; i < numberOfMappings; ++i) { auto path = message->readString(); - auto hash = message->read(SHA256_HASH_LENGTH).toHex(); - BakingStatus status; + auto hash = message->read(AssetUtils::SHA256_HASH_LENGTH).toHex(); + AssetUtils::BakingStatus status; QString lastBakeErrors; message->readPrimitive(&status); - if (status == BakingStatus::Error) { + if (status == AssetUtils::BakingStatus::Error) { lastBakeErrors = message->readString(); } _mappings[path] = { hash, status, lastBakeErrors }; @@ -149,7 +149,7 @@ void GetAllMappingsRequest::doStart() { }); }; -SetMappingRequest::SetMappingRequest(const AssetPath& path, const AssetHash& hash) : +SetMappingRequest::SetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) : _path(path.trimmed()), _hash(hash) { @@ -159,8 +159,8 @@ SetMappingRequest::SetMappingRequest(const AssetPath& path, const AssetHash& has void SetMappingRequest::doStart() { // short circuit the request if the hash or path are invalid - auto validPath = isValidFilePath(_path); - auto validHash = isValidHash(_hash); + auto validPath = AssetUtils::isValidFilePath(_path); + auto validHash = AssetUtils::isValidHash(_hash); if (!validPath || !validHash) { _error = !validPath ? MappingRequest::InvalidPath : MappingRequest::InvalidHash; emit finished(this); @@ -170,17 +170,17 @@ void SetMappingRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->setAssetMapping(_path, _hash, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -193,7 +193,7 @@ void SetMappingRequest::doStart() { }); }; -DeleteMappingsRequest::DeleteMappingsRequest(const AssetPathList& paths) : _paths(paths) { +DeleteMappingsRequest::DeleteMappingsRequest(const AssetUtils::AssetPathList& paths) : _paths(paths) { for (auto& path : _paths) { path = path.trimmed(); } @@ -203,7 +203,7 @@ void DeleteMappingsRequest::doStart() { // short circuit the request if any of the paths are invalid for (auto& path : _paths) { - if (!isValidPath(path)) { + if (!AssetUtils::isValidPath(path)) { _error = MappingRequest::InvalidPath; emit finished(this); return; @@ -213,17 +213,17 @@ void DeleteMappingsRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->deleteAssetMappings(_paths, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -236,7 +236,7 @@ void DeleteMappingsRequest::doStart() { }); }; -RenameMappingRequest::RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) : +RenameMappingRequest::RenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath) : _oldPath(oldPath.trimmed()), _newPath(newPath.trimmed()) { @@ -246,7 +246,7 @@ RenameMappingRequest::RenameMappingRequest(const AssetPath& oldPath, const Asset void RenameMappingRequest::doStart() { // short circuit the request if either of the paths are invalid - if (!isValidFilePath(_oldPath) || !isValidFilePath(_newPath)) { + if (!AssetUtils::isValidFilePath(_oldPath) || !AssetUtils::isValidFilePath(_newPath)) { _error = InvalidPath; emit finished(this); return; @@ -255,17 +255,17 @@ void RenameMappingRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->renameAssetMapping(_oldPath, _newPath, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -278,7 +278,7 @@ void RenameMappingRequest::doStart() { }); } -SetBakingEnabledRequest::SetBakingEnabledRequest(const AssetPathList& paths, bool enabled) : _paths(paths), _enabled(enabled) { +SetBakingEnabledRequest::SetBakingEnabledRequest(const AssetUtils::AssetPathList& paths, bool enabled) : _paths(paths), _enabled(enabled) { for (auto& path : _paths) { path = path.trimmed(); } @@ -288,7 +288,7 @@ void SetBakingEnabledRequest::doStart() { // short circuit the request if any of the paths are invalid for (auto& path : _paths) { - if (!isValidPath(path)) { + if (!AssetUtils::isValidPath(path)) { _error = MappingRequest::InvalidPath; emit finished(this); return; @@ -298,17 +298,17 @@ void SetBakingEnabledRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->setBakingEnabled(_paths, _enabled, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -319,4 +319,4 @@ void SetBakingEnabledRequest::doStart() { emit finished(this); }); -}; \ No newline at end of file +}; diff --git a/libraries/networking/src/MappingRequest.h b/libraries/networking/src/MappingRequest.h index fc43375469..5286849dd1 100644 --- a/libraries/networking/src/MappingRequest.h +++ b/libraries/networking/src/MappingRequest.h @@ -50,10 +50,10 @@ private: class GetMappingRequest : public MappingRequest { Q_OBJECT public: - GetMappingRequest(const AssetPath& path); + GetMappingRequest(const AssetUtils::AssetPath& path); - AssetHash getHash() const { return _hash; } - AssetPath getRedirectedPath() const { return _redirectedPath; } + AssetUtils::AssetHash getHash() const { return _hash; } + AssetUtils::AssetPath getRedirectedPath() const { return _redirectedPath; } bool wasRedirected() const { return _wasRedirected; } signals: @@ -62,21 +62,21 @@ signals: private: virtual void doStart() override; - AssetPath _path; - AssetHash _hash; + AssetUtils::AssetPath _path; + AssetUtils::AssetHash _hash; - AssetPath _redirectedPath; + AssetUtils::AssetPath _redirectedPath; bool _wasRedirected { false }; }; class SetMappingRequest : public MappingRequest { Q_OBJECT public: - SetMappingRequest(const AssetPath& path, const AssetHash& hash); + SetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); - AssetPath getPath() const { return _path; } - AssetHash getHash() const { return _hash; } + AssetUtils::AssetPath getPath() const { return _path; } + AssetUtils::AssetHash getHash() const { return _hash; } signals: void finished(SetMappingRequest* thisRequest); @@ -84,14 +84,14 @@ signals: private: virtual void doStart() override; - AssetPath _path; - AssetHash _hash; + AssetUtils::AssetPath _path; + AssetUtils::AssetHash _hash; }; class DeleteMappingsRequest : public MappingRequest { Q_OBJECT public: - DeleteMappingsRequest(const AssetPathList& path); + DeleteMappingsRequest(const AssetUtils::AssetPathList& path); signals: void finished(DeleteMappingsRequest* thisRequest); @@ -99,13 +99,13 @@ signals: private: virtual void doStart() override; - AssetPathList _paths; + AssetUtils::AssetPathList _paths; }; class RenameMappingRequest : public MappingRequest { Q_OBJECT public: - RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath); + RenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath); signals: void finished(RenameMappingRequest* thisRequest); @@ -113,8 +113,8 @@ signals: private: virtual void doStart() override; - AssetPath _oldPath; - AssetPath _newPath; + AssetUtils::AssetPath _oldPath; + AssetUtils::AssetPath _newPath; }; class GetAllMappingsRequest : public MappingRequest { @@ -122,7 +122,7 @@ class GetAllMappingsRequest : public MappingRequest { public: GetAllMappingsRequest(); - AssetMapping getMappings() const { return _mappings; } + AssetUtils::AssetMapping getMappings() const { return _mappings; } signals: void finished(GetAllMappingsRequest* thisRequest); @@ -130,13 +130,13 @@ signals: private: virtual void doStart() override; - AssetMapping _mappings; + AssetUtils::AssetMapping _mappings; }; class SetBakingEnabledRequest : public MappingRequest { Q_OBJECT public: - SetBakingEnabledRequest(const AssetPathList& path, bool enabled); + SetBakingEnabledRequest(const AssetUtils::AssetPathList& path, bool enabled); signals: void finished(SetBakingEnabledRequest* thisRequest); @@ -144,7 +144,7 @@ signals: private: virtual void doStart() override; - AssetPathList _paths; + AssetUtils::AssetPathList _paths; bool _enabled; }; diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index 2901b4796e..fae645fdbc 100644 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -21,21 +21,25 @@ <@include LightDirectional.slh@> -<@func prepareGlobalLight(isScattering)@> - // prepareGlobalLight - // Transform directions to worldspace - vec3 fragNormal = vec3((normal)); - vec3 fragEyeVector = vec3(invViewMat * vec4(-1.0*position, 0.0)); - vec3 fragEyeDir = normalize(fragEyeVector); - +<@func fetchGlobalLight()@> // Get light - Light light = getLight(); + Light light = getKeyLight(); LightAmbient lightAmbient = getLightAmbient(); vec3 lightDirection = getLightDirection(light); vec3 lightIrradiance = getLightIrradiance(light); vec3 color = vec3(0.0); +<@endfunc@> + +<@func prepareGlobalLight(isScattering)@> + // prepareGlobalLight + // Transform directions to worldspace + vec3 fragNormal = vec3((normal)); + vec3 fragEyeVector = vec3(invViewMat * vec4(-position, 0.0)); + vec3 fragEyeDir = normalize(fragEyeVector); + + <$fetchGlobalLight()$> <@endfunc@> @@ -147,7 +151,7 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu <@func declareEvalLightmappedColor()@> vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 normal, vec3 albedo, vec3 lightmap) { - Light light = getLight(); + Light light = getKeyLight(); LightAmbient ambient = getLightAmbient(); // Catch normals perpendicular to the projection plane, hence the magic number for the threshold @@ -175,11 +179,12 @@ vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscur <$declareLightingAmbient(1, 1, 1)$> <$declareLightingDirectional()$> -vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec3 fresnel, float metallic, vec3 emissive, float roughness, float opacity) { +vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, vec3 normal, vec3 albedo, vec3 fresnel, float metallic, vec3 emissive, float roughness, float opacity, vec3 prevLighting) { <$prepareGlobalLight()$> SurfaceData surface = initSurfaceData(roughness, fragNormal, fragEyeDir); + color = prevLighting; color += emissive * isEmissiveEnabled(); // Ambient @@ -238,6 +243,44 @@ vec3 evalGlobalLightingAlphaBlendedWithHaze( return color; } + +vec3 evalGlobalLightingAlphaBlendedWithHaze( + mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 position, + vec3 albedo, vec3 fresnel, float metallic, vec3 emissive, SurfaceData surface, float opacity, vec3 prevLighting) +{ + <$fetchGlobalLight()$> + + color = prevLighting; + color += emissive * isEmissiveEnabled(); + + // Ambient + vec3 ambientDiffuse; + vec3 ambientSpecular; + evalLightingAmbient(ambientDiffuse, ambientSpecular, lightAmbient, surface, metallic, fresnel, albedo, obscurance); + + // Directional + vec3 directionalDiffuse; + vec3 directionalSpecular; + evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, surface, metallic, fresnel, albedo, shadowAttenuation); + + color += ambientDiffuse + directionalDiffuse; + color += (ambientSpecular + directionalSpecular) / opacity; + + // Haze + if ((hazeParams.hazeMode & HAZE_MODE_IS_ACTIVE) == HAZE_MODE_IS_ACTIVE) { + vec4 colorV4 = computeHazeColor( + vec4(color, 1.0), // fragment original color + position, // fragment position in eye coordinates + surface.eyeDir, // fragment eye vector in world coordinates + invViewMat[3].y, // eye height in world coordinates + lightDirection // keylight direction vector + ); + + color = colorV4.rgb; + } + + return color; +} <@endfunc@> diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 294c7d59d2..14edab86c1 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -45,6 +45,7 @@ using namespace render; struct LightLocations { int radius{ -1 }; + int keyLightBufferUnit{ -1 }; int lightBufferUnit{ -1 }; int ambientBufferUnit { -1 }; int lightIndexBufferUnit { -1 }; @@ -147,9 +148,31 @@ void DeferredLightingEffect::unsetKeyLightBatch(gpu::Batch& batch, int lightBuff } } +void DeferredLightingEffect::setupLocalLightsBatch(gpu::Batch& batch, + int clusterGridBufferUnit, int clusterContentBufferUnit, int frustumGridBufferUnit, + const LightClustersPointer& lightClusters) { + // Bind the global list of lights and the visible lights this frame + batch.setUniformBuffer(_localLightLocations->lightBufferUnit, lightClusters->_lightStage->getLightArrayBuffer()); + + batch.setUniformBuffer(frustumGridBufferUnit, lightClusters->_frustumGridBuffer); + batch.setUniformBuffer(clusterGridBufferUnit, lightClusters->_clusterGridBuffer); + batch.setUniformBuffer(clusterContentBufferUnit, lightClusters->_clusterContentBuffer); +} + +void DeferredLightingEffect::unsetLocalLightsBatch(gpu::Batch& batch, int clusterGridBufferUnit, int clusterContentBufferUnit, int frustumGridBufferUnit) { + if (clusterGridBufferUnit >= 0) { + batch.setUniformBuffer(clusterGridBufferUnit, nullptr); + } + if (clusterContentBufferUnit >= 0) { + batch.setUniformBuffer(clusterContentBufferUnit, nullptr); + } + if (frustumGridBufferUnit >= 0) { + batch.setUniformBuffer(frustumGridBufferUnit, nullptr); + } +} + static gpu::ShaderPointer makeLightProgram(const gpu::ShaderPointer& vertShader, const gpu::ShaderPointer& fragShader, LightLocationsPtr& locations) { gpu::ShaderPointer program = gpu::Shader::createProgram(vertShader, fragShader); - gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("colorMap"), DEFERRED_BUFFER_COLOR_UNIT)); slotBindings.insert(gpu::Shader::Binding(std::string("normalMap"), DEFERRED_BUFFER_NORMAL_UNIT)); @@ -186,6 +209,7 @@ static gpu::ShaderPointer makeLightProgram(const gpu::ShaderPointer& vertShader, locations->texcoordFrameTransform = program->getUniforms().findLocation("texcoordFrameTransform"); + locations->keyLightBufferUnit = program->getUniformBuffers().findLocation("keyLightBuffer"); locations->lightBufferUnit = program->getUniformBuffers().findLocation("lightBuffer"); locations->ambientBufferUnit = program->getUniformBuffers().findLocation("lightAmbientBuffer"); locations->lightIndexBufferUnit = program->getUniformBuffers().findLocation("lightIndexBuffer"); @@ -558,7 +582,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, batch._glUniform4fv(locations->texcoordFrameTransform, 1, reinterpret_cast< const float* >(&textureFrameTransform)); // Setup the global lighting - deferredLightingEffect->setupKeyLightBatch(args, batch, locations->lightBufferUnit, locations->ambientBufferUnit, SKYBOX_MAP_UNIT); + deferredLightingEffect->setupKeyLightBatch(args, batch, locations->keyLightBufferUnit, locations->ambientBufferUnit, SKYBOX_MAP_UNIT); // Haze if (haze) { @@ -567,7 +591,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, batch.draw(gpu::TRIANGLE_STRIP, 4); - deferredLightingEffect->unsetKeyLightBatch(batch, locations->lightBufferUnit, locations->ambientBufferUnit, SKYBOX_MAP_UNIT); + deferredLightingEffect->unsetKeyLightBatch(batch, locations->keyLightBufferUnit, locations->ambientBufferUnit, SKYBOX_MAP_UNIT); for (auto i = 0; i < SHADOW_CASCADE_MAX_COUNT; i++) { batch.setResourceTexture(SHADOW_MAP_UNIT+i, nullptr); @@ -622,12 +646,8 @@ void RenderDeferredLocals::run(const render::RenderContextPointer& renderContext auto& lightIndices = lightClusters->_visibleLightIndices; if (!lightIndices.empty() && lightIndices[0] > 0) { - // Bind the global list of lights and the visible lights this frame - batch.setUniformBuffer(deferredLightingEffect->_localLightLocations->lightBufferUnit, lightClusters->_lightStage->getLightArrayBuffer()); - - batch.setUniformBuffer(LIGHT_CLUSTER_GRID_FRUSTUM_GRID_SLOT, lightClusters->_frustumGridBuffer); - batch.setUniformBuffer(LIGHT_CLUSTER_GRID_CLUSTER_GRID_SLOT, lightClusters->_clusterGridBuffer); - batch.setUniformBuffer(LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT, lightClusters->_clusterContentBuffer); + deferredLightingEffect->setupLocalLightsBatch(batch, LIGHT_CLUSTER_GRID_CLUSTER_GRID_SLOT, LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT, LIGHT_CLUSTER_GRID_FRUSTUM_GRID_SLOT, + lightClusters); // Local light pipeline batch.setPipeline(deferredLightingEffect->_localLight); diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 6d2c0a6819..1b776e6409 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -51,6 +51,9 @@ public: void setupKeyLightBatch(const RenderArgs* args, gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit); void unsetKeyLightBatch(gpu::Batch& batch, int lightBufferUnit, int ambientBufferUnit, int skyboxCubemapUnit); + void setupLocalLightsBatch(gpu::Batch& batch, int clusterGridBufferUnit, int clusterContentBufferUnit, int frustumGridBufferUnit, const LightClustersPointer& lightClusters); + void unsetLocalLightsBatch(gpu::Batch& batch, int clusterGridBufferUnit, int clusterContentBufferUnit, int frustumGridBufferUnit); + void setShadowMapEnabled(bool enable) { _shadowMapEnabled = enable; }; void setAmbientOcclusionEnabled(bool enable) { _ambientOcclusionEnabled = enable; } bool isAmbientOcclusionEnabled() const { return _ambientOcclusionEnabled; } diff --git a/libraries/render-utils/src/DrawHaze.cpp b/libraries/render-utils/src/DrawHaze.cpp index ba6c2e6e8f..c0db1597ca 100644 --- a/libraries/render-utils/src/DrawHaze.cpp +++ b/libraries/render-utils/src/DrawHaze.cpp @@ -147,7 +147,7 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), HazeEffect_TransformBufferSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("colorMap"), HazeEffect_ColorMapSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), HazeEffect_LinearDepthMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), HazeEffect_LightingMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("keyLightBuffer"), HazeEffect_LightingMapSlot)); gpu::Shader::makeProgram(*program, slotBindings); _hazePipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); diff --git a/libraries/render-utils/src/ForwardGlobalLight.slh b/libraries/render-utils/src/ForwardGlobalLight.slh index e86f0c7fe3..a55e2ea651 100644 --- a/libraries/render-utils/src/ForwardGlobalLight.slh +++ b/libraries/render-utils/src/ForwardGlobalLight.slh @@ -29,7 +29,7 @@ vec3 fragEyeDir = normalize(fragEyeVector); // Get light - Light light = getLight(); + Light light = getKeyLight(); LightAmbient lightAmbient = getLightAmbient(); vec3 lightDirection = getLightDirection(light); @@ -143,7 +143,7 @@ vec3 evalSkyboxGlobalColor(mat4 invViewMat, float shadowAttenuation, float obscu <@func declareEvalLightmappedColor()@> vec3 evalLightmappedColor(mat4 invViewMat, float shadowAttenuation, float obscurance, vec3 normal, vec3 albedo, vec3 lightmap) { - Light light = getLight(); + Light light = getKeyLight(); LightAmbient ambient = getLightAmbient(); // Catch normals perpendicular to the projection plane, hence the magic number for the threshold diff --git a/libraries/render-utils/src/Haze.slf b/libraries/render-utils/src/Haze.slf index 2d7daf1f98..e254266350 100644 --- a/libraries/render-utils/src/Haze.slf +++ b/libraries/render-utils/src/Haze.slf @@ -53,7 +53,7 @@ void main(void) { vec4 worldFragPos = viewInverse * eyeFragPos; vec4 worldEyePos = viewInverse[3]; - Light light = getLight(); + Light light = getKeyLight(); vec3 lightDirection = getLightDirection(light); outFragColor = computeHazeColor(fragColor, eyeFragPos.xyz, worldFragPos.xyz, worldEyePos.y, lightDirection); diff --git a/libraries/render-utils/src/LightClusterGrid.slh b/libraries/render-utils/src/LightClusterGrid.slh index 722d37814d..709e8c0f70 100644 --- a/libraries/render-utils/src/LightClusterGrid.slh +++ b/libraries/render-utils/src/LightClusterGrid.slh @@ -86,4 +86,24 @@ int clusterGrid_getClusterLightId(int index, int offset) { return (((elementIndex & 0x00000001) == 1) ? (element >> 16) : element) & 0x0000FFFF; } + +<@func fetchClusterInfo(fragWorldPos)@> + + // From frag world pos find the cluster + vec4 clusterEyePos = frustumGrid_worldToEye(<$fragWorldPos$>); + ivec3 clusterPos = frustumGrid_eyeToClusterPos(clusterEyePos.xyz); + + ivec3 cluster = clusterGrid_getCluster(frustumGrid_clusterToIndex(clusterPos)); + int numLights = cluster.x + cluster.y; + ivec3 dims = frustumGrid.dims.xyz; + +<@endfunc@> + +bool hasLocalLights(int numLights, ivec3 clusterPos, ivec3 dims) { + return numLights>0 + && all(greaterThanEqual(clusterPos, ivec3(0))) + && all(lessThan(clusterPos.xy, dims.xy)) + && clusterPos.z <= dims.z; +} + <@endif@> diff --git a/libraries/render-utils/src/LightLocal.slh b/libraries/render-utils/src/LightLocal.slh new file mode 100644 index 0000000000..515a065d2e --- /dev/null +++ b/libraries/render-utils/src/LightLocal.slh @@ -0,0 +1,148 @@ +// Generated on <$_SCRIBE_DATE$> +// +// Created by Olivier Prat on 15/01/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Everything about light +<@include graphics/Light.slh@> +<$declareLightBuffer(256)$> +<@include LightingModel.slh@> + + +<@include LightPoint.slh@> +<$declareLightingPoint(supportScattering)$> +<@include LightSpot.slh@> +<$declareLightingSpot(supportScattering)$> + +<@include LightClusterGrid.slh@> + +vec4 evalLocalLighting(ivec3 cluster, int numLights, vec3 fragWorldPos, SurfaceData surface, + float fragMetallic, vec3 fragFresnel, vec3 fragAlbedo, float fragScattering, + vec4 midNormalCurvature, vec4 lowNormalCurvature, float opacity) { + vec4 fragColor = vec4(0.0); + vec3 fragSpecular = vec3(0.0); + vec3 fragDiffuse = vec3(0.0); + int lightClusterOffset = cluster.z; + + // Compute the rougness into gloss2 once: + bool withScattering = (fragScattering * isScatteringEnabled() > 0.0); + + int numLightTouching = 0; + for (int i = 0; i < cluster.x; i++) { + // Need the light now + int theLightIndex = clusterGrid_getClusterLightId(i, lightClusterOffset); + Light light = getLight(theLightIndex); + + // Clip againgst the light volume and Make the Light vector going from fragment to light center in world space + vec4 fragLightVecLen2; + vec4 fragLightDirLen; + + if (!lightVolume_clipFragToLightVolumePoint(light.volume, fragWorldPos.xyz, fragLightVecLen2)) { + continue; + } + + // Allright we re in the light sphere volume + fragLightDirLen.w = length(fragLightVecLen2.xyz); + fragLightDirLen.xyz = fragLightVecLen2.xyz / fragLightDirLen.w; + if (dot(surface.normal, fragLightDirLen.xyz) < 0.0) { + continue; + } + + numLightTouching++; + + vec3 diffuse = vec3(1.0); + vec3 specular = vec3(0.1); + + // Allright we re valid in the volume + float fragLightDistance = fragLightDirLen.w; + vec3 fragLightDir = fragLightDirLen.xyz; + + updateSurfaceDataWithLight(surface, fragLightDir); + + // Eval attenuation + float radialAttenuation = lightIrradiance_evalLightAttenuation(light.irradiance, fragLightDistance); + vec3 lightEnergy = radialAttenuation * getLightIrradiance(light); + + // Eval shading + if (withScattering) { + evalFragShadingScattering(diffuse, specular, fragMetallic, fragFresnel, surface, fragAlbedo, + fragScattering, midNormalCurvature, lowNormalCurvature ); + } else { + evalFragShadingGloss(diffuse, specular, fragMetallic, fragFresnel, surface, fragAlbedo); + } + + diffuse *= lightEnergy; + specular *= lightEnergy; + + fragDiffuse.rgb += diffuse; + fragSpecular.rgb += specular; + } + + for (int i = cluster.x; i < numLights; i++) { + // Need the light now + int theLightIndex = clusterGrid_getClusterLightId(i, lightClusterOffset); + Light light = getLight(theLightIndex); + + // Clip againgst the light volume and Make the Light vector going from fragment to light center in world space + vec4 fragLightVecLen2; + vec4 fragLightDirLen; + float cosSpotAngle; + + if (!lightVolume_clipFragToLightVolumePoint(light.volume, fragWorldPos.xyz, fragLightVecLen2)) { + continue; + } + + // Allright we re in the light sphere volume + fragLightDirLen.w = length(fragLightVecLen2.xyz); + fragLightDirLen.xyz = fragLightVecLen2.xyz / fragLightDirLen.w; + if (dot(surface.normal, fragLightDirLen.xyz) < 0.0) { + continue; + } + + // Check spot + if (!lightVolume_clipFragToLightVolumeSpotSide(light.volume, fragLightDirLen, cosSpotAngle)) { + continue; + } + + numLightTouching++; + + vec3 diffuse = vec3(1.0); + vec3 specular = vec3(0.1); + + // Allright we re valid in the volume + float fragLightDistance = fragLightDirLen.w; + vec3 fragLightDir = fragLightDirLen.xyz; + + updateSurfaceDataWithLight(surface, fragLightDir); + + // Eval attenuation + float radialAttenuation = lightIrradiance_evalLightAttenuation(light.irradiance, fragLightDistance); + float angularAttenuation = lightIrradiance_evalLightSpotAttenuation(light.irradiance, cosSpotAngle); + vec3 lightEnergy = radialAttenuation * angularAttenuation * getLightIrradiance(light); + + // Eval shading + if (withScattering) { + evalFragShadingScattering(diffuse, specular, fragMetallic, fragFresnel, surface, fragAlbedo, + fragScattering, midNormalCurvature, lowNormalCurvature ); + } else { + evalFragShadingGloss(diffuse, specular, fragMetallic, fragFresnel, surface, fragAlbedo); + } + + diffuse *= lightEnergy; + specular *= lightEnergy; + + fragDiffuse.rgb += diffuse; + fragSpecular.rgb += specular; + } + + fragDiffuse *= isDiffuseEnabled(); + fragSpecular *= isSpecularEnabled(); + + fragColor.rgb += fragDiffuse; + fragColor.rgb += fragSpecular / opacity; + return fragColor; +} \ No newline at end of file diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index be8330f198..ddcbf8229c 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -289,9 +289,8 @@ void evalFragShading(out vec3 diffuse, out vec3 specular, void evalFragShadingScattering(out vec3 diffuse, out vec3 specular, - float metallic, vec3 fresnel, SurfaceData surface, vec3 albedo - ,float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature -) { + float metallic, vec3 fresnel, SurfaceData surface, vec3 albedo, + float scattering, vec4 midNormalCurvature, vec4 lowNormalCurvature) { vec3 brdf = evalSkinBRDF(surface.lightDir, surface.normal, midNormalCurvature.xyz, lowNormalCurvature.xyz, lowNormalCurvature.w); float NdotL = surface.ndotl; diffuse = mix(vec3(NdotL), brdf, scattering); @@ -305,8 +304,7 @@ void evalFragShadingScattering(out vec3 diffuse, out vec3 specular, } void evalFragShadingGloss(out vec3 diffuse, out vec3 specular, - float metallic, vec3 fresnel, SurfaceData surface, vec3 albedo -) { + float metallic, vec3 fresnel, SurfaceData surface, vec3 albedo) { vec4 shading = evalPBRShading(metallic, fresnel, surface); diffuse = vec3(shading.w); diffuse *= mix(vec3(1.0), albedo, isAlbedoEnabled()); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 45c77a606d..b0763c0fb3 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -422,8 +422,8 @@ void Model::initJointStates() { } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, - QString& extraInfo, bool pickAgainstTriangles, bool allowBackface) { + BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, + bool pickAgainstTriangles, bool allowBackface) { bool intersectedSomething = false; @@ -454,6 +454,10 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g QMutexLocker locker(&_mutex); float bestDistance = std::numeric_limits::max(); + Triangle bestModelTriangle; + Triangle bestWorldTriangle; + int bestSubMeshIndex = 0; + int subMeshIndex = 0; const FBXGeometry& geometry = getFBXGeometry(); @@ -471,8 +475,8 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g for (auto& triangleSet : _modelSpaceMeshTriangleSets) { float triangleSetDistance = 0.0f; BoxFace triangleSetFace; - glm::vec3 triangleSetNormal; - if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles, allowBackface)) { + Triangle triangleSetTriangle; + if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetTriangle, pickAgainstTriangles, allowBackface)) { glm::vec3 meshIntersectionPoint = meshFrameOrigin + (meshFrameDirection * triangleSetDistance); glm::vec3 worldIntersectionPoint = glm::vec3(meshToWorldMatrix * glm::vec4(meshIntersectionPoint, 1.0f)); @@ -482,8 +486,11 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g bestDistance = worldDistance; intersectedSomething = true; face = triangleSetFace; - surfaceNormal = glm::vec3(meshToWorldMatrix * glm::vec4(triangleSetNormal, 0.0f)); - extraInfo = geometry.getModelNameOfMesh(subMeshIndex); + bestModelTriangle = triangleSetTriangle; + bestWorldTriangle = triangleSetTriangle * meshToWorldMatrix; + extraInfo["worldIntersectionPoint"] = vec3toVariant(worldIntersectionPoint); + extraInfo["meshIntersectionPoint"] = vec3toVariant(meshIntersectionPoint); + bestSubMeshIndex = subMeshIndex; } } subMeshIndex++; @@ -491,9 +498,24 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g if (intersectedSomething) { distance = bestDistance; + surfaceNormal = bestWorldTriangle.getNormal(); + if (pickAgainstTriangles) { + extraInfo["subMeshIndex"] = bestSubMeshIndex; + extraInfo["subMeshName"] = geometry.getModelNameOfMesh(bestSubMeshIndex); + extraInfo["subMeshTriangleWorld"] = QVariantMap{ + { "v0", vec3toVariant(bestWorldTriangle.v0) }, + { "v1", vec3toVariant(bestWorldTriangle.v1) }, + { "v2", vec3toVariant(bestWorldTriangle.v2) }, + }; + extraInfo["subMeshNormal"] = vec3toVariant(bestModelTriangle.getNormal()); + extraInfo["subMeshTriangle"] = QVariantMap{ + { "v0", vec3toVariant(bestModelTriangle.v0) }, + { "v1", vec3toVariant(bestModelTriangle.v1) }, + { "v2", vec3toVariant(bestModelTriangle.v2) }, + }; + } + } - - return intersectedSomething; } return intersectedSomething; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 560aa82f0c..27eaedec40 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -161,8 +161,8 @@ public: void setJointTranslation(int index, bool valid, const glm::vec3& translation, float priority); bool findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, - QString& extraInfo, bool pickAgainstTriangles = false, bool allowBackface = false); + BoxFace& face, glm::vec3& surfaceNormal, + QVariantMap& extraInfo, bool pickAgainstTriangles = false, bool allowBackface = false); void setOffset(const glm::vec3& offset); const glm::vec3& getOffset() const { return _offset; } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index a3c01f7fb3..f262307944 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -12,6 +12,8 @@ #include "RenderDeferredTask.h" +#include + #include #include #include @@ -168,7 +170,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawHazeDeferred", drawHazeInputs); // Render transparent objects forward in LightingBuffer - const auto transparentsInputs = DrawDeferred::Inputs(transparents, lightingModel).asVarying(); + const auto transparentsInputs = DrawDeferred::Inputs(transparents, lightingModel, lightClusters).asVarying(); task.addJob("DrawTransparentDeferred", transparentsInputs, shapePlumber); // Light Cluster Grid Debuging job @@ -298,6 +300,8 @@ void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& const auto& inItems = inputs.get0(); const auto& lightingModel = inputs.get1(); + const auto& lightClusters = inputs.get2(); + auto deferredLightingEffect = DependencyManager::get(); RenderArgs* args = renderContext->args; @@ -319,7 +323,13 @@ void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - // Setup haze iff current zone has haze + deferredLightingEffect->setupLocalLightsBatch(batch, + render::ShapePipeline::Slot::LIGHT_CLUSTER_GRID_CLUSTER_GRID_SLOT, + render::ShapePipeline::Slot::LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT, + render::ShapePipeline::Slot::LIGHT_CLUSTER_GRID_FRUSTUM_GRID_SLOT, + lightClusters); + + // Setup haze if current zone has haze auto hazeStage = args->_scene->getStage(); if (hazeStage && hazeStage->_currentFrame._hazes.size() > 0) { graphics::HazePointer hazePointer = hazeStage->getHaze(hazeStage->_currentFrame._hazes.front()); @@ -341,6 +351,11 @@ void DrawDeferred::run(const RenderContextPointer& renderContext, const Inputs& args->_batch = nullptr; args->_globalShapeKey = 0; + + deferredLightingEffect->unsetLocalLightsBatch(batch, + render::ShapePipeline::Slot::LIGHT_CLUSTER_GRID_CLUSTER_GRID_SLOT, + render::ShapePipeline::Slot::LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT, + render::ShapePipeline::Slot::LIGHT_CLUSTER_GRID_FRUSTUM_GRID_SLOT); }); config->setNumDrawn((int)inItems.size()); diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 00804f7e7b..1b57c88768 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -15,6 +15,7 @@ #include #include #include "LightingModel.h" +#include "LightClusters.h" class DrawDeferredConfig : public render::Job::Config { Q_OBJECT @@ -40,7 +41,7 @@ protected: class DrawDeferred { public: - using Inputs = render::VaryingSet2; + using Inputs = render::VaryingSet3 ; using Config = DrawDeferredConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index cdd6f2b805..032617f22c 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -29,6 +29,8 @@ #include "model_lightmap_fade_vert.h" #include "model_lightmap_normal_map_fade_vert.h" +#include "model_translucent_vert.h" +#include "model_translucent_fade_vert.h" #include "skin_model_fade_vert.h" #include "skin_model_normal_map_fade_vert.h" @@ -188,6 +190,8 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip auto modelNormalMapVertex = model_normal_map_vert::getShader(); auto modelLightmapVertex = model_lightmap_vert::getShader(); auto modelLightmapNormalMapVertex = model_lightmap_normal_map_vert::getShader(); + auto modelTranslucentVertex = model_translucent_vert::getShader(); + auto modelTranslucentFadeVertex = model_translucent_fade_vert::getShader(); auto modelShadowVertex = model_shadow_vert::getShader(); auto skinModelVertex = skin_model_vert::getShader(); auto skinModelNormalMapVertex = skin_model_normal_map_vert::getShader(); @@ -196,6 +200,8 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip auto modelLightmapNormalMapFadeVertex = model_lightmap_normal_map_fade_vert::getShader(); auto skinModelFadeVertex = skin_model_fade_vert::getShader(); auto skinModelNormalMapFadeVertex = skin_model_normal_map_fade_vert::getShader(); + auto skinModelTranslucentVertex = skinModelFadeVertex; // We use the same because it ouputs world position per vertex + auto skinModelNormalMapTranslucentVertex = skinModelNormalMapFadeVertex; // We use the same because it ouputs world position per vertex auto modelFadeVertex = model_fade_vert::getShader(); auto modelNormalMapFadeVertex = model_normal_map_fade_vert::getShader(); @@ -289,7 +295,7 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip // Translucents addPipeline( Key::Builder().withMaterial().withTranslucent(), - modelVertex, modelTranslucentPixel, nullptr, nullptr); + modelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); addPipeline( Key::Builder().withTranslucent(), simpleVertex, simpleTranslucentPixel, nullptr, nullptr); @@ -301,21 +307,21 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip simpleVertex, simpleTranslucentUnlitPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withTranslucent().withTangents(), - modelNormalMapVertex, modelTranslucentPixel, nullptr, nullptr); + modelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withTranslucent().withSpecular(), - modelVertex, modelTranslucentPixel, nullptr, nullptr); + modelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withTranslucent().withTangents().withSpecular(), - modelNormalMapVertex, modelTranslucentPixel, nullptr, nullptr); + modelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); addPipeline( // FIXME: Ignore lightmap for translucents meshpart Key::Builder().withMaterial().withTranslucent().withLightmap(), - modelVertex, modelTranslucentPixel, nullptr, nullptr); + modelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); // Same thing but with Fade on addPipeline( Key::Builder().withMaterial().withTranslucent().withFade(), - modelFadeVertex, modelTranslucentFadePixel, batchSetter, itemSetter); + modelTranslucentFadeVertex, modelTranslucentFadePixel, batchSetter, itemSetter); addPipeline( Key::Builder().withTranslucent().withFade(), simpleFadeVertex, simpleTranslucentFadePixel, batchSetter, itemSetter); @@ -396,16 +402,16 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip // Skinned and Translucent addPipeline( Key::Builder().withMaterial().withSkinned().withTranslucent(), - skinModelVertex, modelTranslucentPixel, nullptr, nullptr); + skinModelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withSkinned().withTranslucent().withTangents(), - skinModelNormalMapVertex, modelTranslucentPixel, nullptr, nullptr); + skinModelNormalMapTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withSkinned().withTranslucent().withSpecular(), - skinModelVertex, modelTranslucentPixel, nullptr, nullptr); + skinModelTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withSkinned().withTranslucent().withTangents().withSpecular(), - skinModelNormalMapVertex, modelTranslucentPixel, nullptr, nullptr); + skinModelNormalMapTranslucentVertex, modelTranslucentPixel, nullptr, nullptr); // Same thing but with Fade on addPipeline( Key::Builder().withMaterial().withSkinned().withTranslucent().withFade(), @@ -571,9 +577,9 @@ void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderAr batchSetter(pipeline, batch, args); // Set the light - if (pipeline.locations->lightBufferUnit >= 0) { + if (pipeline.locations->keyLightBufferUnit >= 0) { DependencyManager::get()->setupKeyLightBatch(args, batch, - pipeline.locations->lightBufferUnit, + pipeline.locations->keyLightBufferUnit, pipeline.locations->lightAmbientBufferUnit, pipeline.locations->lightAmbientMapUnit); } diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index d6ec73da85..ff415accc3 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -463,7 +463,7 @@ gpu::PipelinePointer DebugSubsurfaceScattering::getScatteringPipeline() { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ScatteringTask_FrameTransformSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("scatteringParamsBuffer"), ScatteringTask_ParamSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), ScatteringTask_LightSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("keyLightBuffer"), ScatteringTask_LightSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("scatteringLUT"), ScatteringTask_ScatteringTableSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("curvatureMap"), ScatteringTask_CurvatureMapSlot)); diff --git a/libraries/render-utils/src/ZoneRenderer.cpp b/libraries/render-utils/src/ZoneRenderer.cpp index 165ff55f4b..1d358ff895 100644 --- a/libraries/render-utils/src/ZoneRenderer.cpp +++ b/libraries/render-utils/src/ZoneRenderer.cpp @@ -83,7 +83,7 @@ const gpu::PipelinePointer& DebugZoneLighting::getKeyLightPipeline() { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), ZONE_DEFERRED_TRANSFORM_BUFFER)); - slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), ZONE_KEYLIGHT_BUFFER)); + slotBindings.insert(gpu::Shader::Binding(std::string("keyLightBuffer"), ZONE_KEYLIGHT_BUFFER)); gpu::Shader::makeProgram(*program, slotBindings); diff --git a/libraries/render-utils/src/local_lights_drawOutline.slf b/libraries/render-utils/src/local_lights_drawOutline.slf index 56ce1e61a5..38dd383459 100644 --- a/libraries/render-utils/src/local_lights_drawOutline.slf +++ b/libraries/render-utils/src/local_lights_drawOutline.slf @@ -54,26 +54,8 @@ void main(void) { mat4 invViewMat = getViewInverse(); vec4 fragPos = invViewMat * fragPosition; - // From frag world pos find the cluster - vec4 clusterEyePos = frustumGrid_worldToEye(fragPos); - ivec3 clusterPos = frustumGrid_eyeToClusterPos(clusterEyePos.xyz); - - ivec3 cluster = clusterGrid_getCluster(frustumGrid_clusterToIndex(clusterPos)); - int numLights = cluster.x + cluster.y; - if (numLights <= 0) { - discard; - } - int lightClusterOffset = cluster.z; - - ivec3 dims = frustumGrid.dims.xyz; - if (clusterPos.x < 0 || clusterPos.x >= dims.x) { - discard; - } - - if (clusterPos.y < 0 || clusterPos.y >= dims.y) { - discard; - } - if (clusterPos.z < 0 || clusterPos.z > dims.z) { + <$fetchClusterInfo(fragPos)$>; + if (!hasLocalLights(numLights, clusterPos, dims)) { discard; } @@ -82,6 +64,7 @@ void main(void) { vec3 fragEyeDir = normalize(fragEyeVector.xyz); int numLightTouching = 0; + int lightClusterOffset = cluster.z; for (int i = 0; i < cluster.x; i++) { // Need the light now int theLightIndex = clusterGrid_getClusterLightId(i, lightClusterOffset); diff --git a/libraries/render-utils/src/local_lights_shading.slf b/libraries/render-utils/src/local_lights_shading.slf index c3208de726..8ae8f3fb5b 100644 --- a/libraries/render-utils/src/local_lights_shading.slf +++ b/libraries/render-utils/src/local_lights_shading.slf @@ -17,18 +17,7 @@ <$declareDeferredCurvature()$> -// Everything about light -<@include graphics/Light.slh@> -<$declareLightBuffer(256)$> -<@include LightingModel.slh@> - - -<@include LightPoint.slh@> -<$declareLightingPoint(supportScattering)$> -<@include LightSpot.slh@> -<$declareLightingSpot(supportScattering)$> - -<@include LightClusterGrid.slh@> +<@include LightLocal.slh@> in vec2 _texCoord0; out vec4 _fragColor; @@ -49,28 +38,10 @@ void main(void) { // Frag pos in world mat4 invViewMat = getViewInverse(); - vec4 fragPos = invViewMat * fragPosition; + vec4 fragWorldPos = invViewMat * fragPosition; - // From frag world pos find the cluster - vec4 clusterEyePos = frustumGrid_worldToEye(fragPos); - ivec3 clusterPos = frustumGrid_eyeToClusterPos(clusterEyePos.xyz); - - ivec3 cluster = clusterGrid_getCluster(frustumGrid_clusterToIndex(clusterPos)); - int numLights = cluster.x + cluster.y; - if (numLights <= 0) { - discard; - } - int lightClusterOffset = cluster.z; - - ivec3 dims = frustumGrid.dims.xyz; - if (clusterPos.x < 0 || clusterPos.x >= dims.x) { - discard; - } - - if (clusterPos.y < 0 || clusterPos.y >= dims.y) { - discard; - } - if (clusterPos.z < 0 || clusterPos.z > dims.z) { + <$fetchClusterInfo(fragWorldPos)$>; + if (!hasLocalLights(numLights, clusterPos, dims)) { discard; } @@ -84,117 +55,11 @@ void main(void) { // Frag to eye vec vec4 fragEyeVector = invViewMat * vec4(-frag.position.xyz, 0.0); vec3 fragEyeDir = normalize(fragEyeVector.xyz); - SurfaceData surface = initSurfaceData(frag.roughness, frag.normal, fragEyeDir); - bool withScattering = (frag.scattering * isScatteringEnabled() > 0.0); - int numLightTouching = 0; - for (int i = 0; i < cluster.x; i++) { - // Need the light now - int theLightIndex = clusterGrid_getClusterLightId(i, lightClusterOffset); - Light light = getLight(theLightIndex); - - // Clip againgst the light volume and Make the Light vector going from fragment to light center in world space - vec4 fragLightVecLen2; - vec4 fragLightDirLen; - - if (!lightVolume_clipFragToLightVolumePoint(light.volume, fragPos.xyz, fragLightVecLen2)) { - continue; - } - - // Allright we re in the light sphere volume - fragLightDirLen.w = length(fragLightVecLen2.xyz); - fragLightDirLen.xyz = fragLightVecLen2.xyz / fragLightDirLen.w; - if (dot(frag.normal, fragLightDirLen.xyz) < 0.0) { - continue; - } - - numLightTouching++; - - vec3 diffuse = vec3(1.0); - vec3 specular = vec3(0.1); - - // Allright we re valid in the volume - float fragLightDistance = fragLightDirLen.w; - vec3 fragLightDir = fragLightDirLen.xyz; - - updateSurfaceDataWithLight(surface, fragLightDir); - - // Eval attenuation - float radialAttenuation = lightIrradiance_evalLightAttenuation(light.irradiance, fragLightDistance); - vec3 lightEnergy = radialAttenuation * getLightIrradiance(light); - - // Eval shading - if (withScattering) { - evalFragShadingScattering(diffuse, specular, frag.metallic, frag.fresnel, surface, frag.albedo - ,frag.scattering, midNormalCurvature, lowNormalCurvature ); - } else { - evalFragShadingGloss(diffuse, specular, frag.metallic, frag.fresnel, surface, frag.albedo); - } - - diffuse *= lightEnergy * isDiffuseEnabled(); - specular *= lightEnergy * isSpecularEnabled(); - - _fragColor.rgb += diffuse; - _fragColor.rgb += specular; - } - - for (int i = cluster.x; i < numLights; i++) { - // Need the light now - int theLightIndex = clusterGrid_getClusterLightId(i, lightClusterOffset); - Light light = getLight(theLightIndex); - - // Clip againgst the light volume and Make the Light vector going from fragment to light center in world space - vec4 fragLightVecLen2; - vec4 fragLightDirLen; - float cosSpotAngle; - - if (!lightVolume_clipFragToLightVolumePoint(light.volume, fragPos.xyz, fragLightVecLen2)) { - continue; - } - - // Allright we re in the light sphere volume - fragLightDirLen.w = length(fragLightVecLen2.xyz); - fragLightDirLen.xyz = fragLightVecLen2.xyz / fragLightDirLen.w; - if (dot(frag.normal, fragLightDirLen.xyz) < 0.0) { - continue; - } - - // Check spot - if (!lightVolume_clipFragToLightVolumeSpotSide(light.volume, fragLightDirLen, cosSpotAngle)) { - continue; - } - - numLightTouching++; - - vec3 diffuse = vec3(1.0); - vec3 specular = vec3(0.1); - - // Allright we re valid in the volume - float fragLightDistance = fragLightDirLen.w; - vec3 fragLightDir = fragLightDirLen.xyz; - - updateSurfaceDataWithLight(surface, fragLightDir); - - // Eval attenuation - float radialAttenuation = lightIrradiance_evalLightAttenuation(light.irradiance, fragLightDistance); - float angularAttenuation = lightIrradiance_evalLightSpotAttenuation(light.irradiance, cosSpotAngle); - vec3 lightEnergy = radialAttenuation * angularAttenuation * getLightIrradiance(light); - - // Eval shading - if (withScattering) { - evalFragShadingScattering(diffuse, specular, frag.metallic, frag.fresnel, surface, frag.albedo - ,frag.scattering, midNormalCurvature, lowNormalCurvature ); - } else { - evalFragShadingGloss(diffuse, specular, frag.metallic, frag.fresnel, surface, frag.albedo); - } - - diffuse *= lightEnergy * isDiffuseEnabled(); - specular *= lightEnergy * isSpecularEnabled(); - - _fragColor.rgb += diffuse; - _fragColor.rgb += specular; - } + _fragColor = evalLocalLighting(cluster, numLights, fragWorldPos.xyz, surface, + frag.metallic, frag.fresnel, frag.albedo, frag.scattering, + midNormalCurvature, lowNormalCurvature, 1.0); } diff --git a/libraries/render-utils/src/model_translucent.slf b/libraries/render-utils/src/model_translucent.slf index 0924389dba..761e702595 100644 --- a/libraries/render-utils/src/model_translucent.slf +++ b/libraries/render-utils/src/model_translucent.slf @@ -18,6 +18,8 @@ <$declareEvalGlobalLightingAlphaBlended()$> +<@include LightLocal.slh@> + <@include gpu/Transform.slh@> <$declareStandardCameraTransform()$> @@ -27,6 +29,7 @@ in vec2 _texCoord0; in vec2 _texCoord1; in vec4 _position; +in vec4 _worldPosition; in vec3 _normal; in vec3 _color; in float _alpha; @@ -56,20 +59,32 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 fragPosition = _position.xyz; + // Lighting is done in world space vec3 fragNormal = normalize(_normal); TransformCamera cam = getTransformCamera(); + vec3 fragEyeVector = vec3(cam._viewInverse * vec4(-fragPosition, 0.0)); + vec3 fragEyeDir = normalize(fragEyeVector); + SurfaceData surface = initSurfaceData(roughness, fragNormal, fragEyeDir); + + vec4 localLighting = vec4(0.0); + + <$fetchClusterInfo(_worldPosition)$>; + if (hasLocalLights(numLights, clusterPos, dims)) { + localLighting = evalLocalLighting(cluster, numLights, _worldPosition.xyz, surface, + metallic, fresnel, albedo, 0.0, + vec4(0), vec4(0), opacity); + } _fragColor = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, occlusionTex, fragPosition, - fragNormal, albedo, fresnel, metallic, emissive, - roughness, opacity), + surface, opacity, localLighting.rgb), opacity); } diff --git a/libraries/render-utils/src/model_translucent.slv b/libraries/render-utils/src/model_translucent.slv new file mode 100644 index 0000000000..305aba06c3 --- /dev/null +++ b/libraries/render-utils/src/model_translucent.slv @@ -0,0 +1,44 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// model_translucent.slv +// vertex shader +// +// Created by Olivier Prat on 15/01/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Color.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include MaterialTextures.slh@> +<$declareMaterialTexMapArrayBuffer()$> + +out float _alpha; +out vec2 _texCoord0; +out vec2 _texCoord1; +out vec4 _position; +out vec4 _worldPosition; +out vec3 _normal; +out vec3 _color; + +void main(void) { + _color = colorToLinearRGB(inColor.xyz); + _alpha = inColor.w; + + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> + <$transformModelToWorldPos(obj, inPosition, _worldPosition)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> +} diff --git a/libraries/render-utils/src/model_translucent_fade.slf b/libraries/render-utils/src/model_translucent_fade.slf index 8ac442476f..a2d271653c 100644 --- a/libraries/render-utils/src/model_translucent_fade.slf +++ b/libraries/render-utils/src/model_translucent_fade.slf @@ -18,6 +18,8 @@ <$declareEvalGlobalLightingAlphaBlended()$> +<@include LightLocal.slh@> + <@include gpu/Transform.slh@> <$declareStandardCameraTransform()$> @@ -66,20 +68,32 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 fragPosition = _position.xyz; + // Lighting is done in world space vec3 fragNormal = normalize(_normal); TransformCamera cam = getTransformCamera(); + vec3 fragEyeVector = vec3(cam._viewInverse * vec4(-fragPosition, 0.0)); + vec3 fragEyeDir = normalize(fragEyeVector); + SurfaceData surface = initSurfaceData(roughness, fragNormal, fragEyeDir); + + vec4 localLighting = vec4(0.0); + + <$fetchClusterInfo(_worldPosition)$>; + if (hasLocalLights(numLights, clusterPos, dims)) { + localLighting = evalLocalLighting(cluster, numLights, _worldPosition.xyz, surface, + metallic, fresnel, albedo, 0.0, + vec4(0), vec4(0), opacity); + } _fragColor = vec4(evalGlobalLightingAlphaBlendedWithHaze( cam._viewInverse, 1.0, occlusionTex, fragPosition, - fragNormal, albedo, fresnel, metallic, emissive+fadeEmissive, - roughness, opacity), + surface, opacity, localLighting.rgb), opacity); } diff --git a/libraries/render-utils/src/model_translucent_fade.slv b/libraries/render-utils/src/model_translucent_fade.slv new file mode 100644 index 0000000000..3fb9ad2cb4 --- /dev/null +++ b/libraries/render-utils/src/model_translucent_fade.slv @@ -0,0 +1,44 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// model_translucent_fade.slv +// vertex shader +// +// Created by Olivier Prat on 15/01/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> +<@include gpu/Color.slh@> +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include MaterialTextures.slh@> +<$declareMaterialTexMapArrayBuffer()$> + +out float _alpha; +out vec2 _texCoord0; +out vec2 _texCoord1; +out vec4 _position; +out vec4 _worldPosition; +out vec3 _normal; +out vec3 _color; + +void main(void) { + _color = colorToLinearRGB(inColor.xyz); + _alpha = inColor.w; + + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _texCoord0)$> + <$evalTexMapArrayTexcoord1(texMapArray, inTexCoord0, _texCoord1)$> + + // standard transform + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> + <$transformModelToWorldPos(obj, inPosition, _worldPosition)$> + <$transformModelToWorldDir(cam, obj, inNormal.xyz, _normal)$> +} diff --git a/libraries/render-utils/src/overlay3D.slf b/libraries/render-utils/src/overlay3D.slf index dcded917b4..b6df71ccc3 100644 --- a/libraries/render-utils/src/overlay3D.slf +++ b/libraries/render-utils/src/overlay3D.slf @@ -27,7 +27,7 @@ vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 fresnel, float roughness, float opacity) { // Need the light now - Light light = getLight(); + Light light = getKeyLight(); vec3 lightDirection = getLightDirection(light); vec3 lightIrradiance = getLightIrradiance(light); diff --git a/libraries/render-utils/src/overlay3D_translucent.slf b/libraries/render-utils/src/overlay3D_translucent.slf index 64035ac984..c247364a8c 100644 --- a/libraries/render-utils/src/overlay3D_translucent.slf +++ b/libraries/render-utils/src/overlay3D_translucent.slf @@ -27,7 +27,7 @@ vec4 evalGlobalColor(float shadowAttenuation, vec3 position, vec3 normal, vec3 albedo, float metallic, vec3 fresnel, float roughness, float opacity) { // Need the light now - Light light = getLight(); + Light light = getKeyLight(); vec3 lightDirection = getLightDirection(light); vec3 lightIrradiance = getLightIrradiance(light); diff --git a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf index ad6918d727..e66d881e11 100644 --- a/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf +++ b/libraries/render-utils/src/subsurfaceScattering_drawScattering.slf @@ -42,7 +42,7 @@ vec3 evalScatteringBRDF(vec2 texcoord) { vec3 fragNormal = vec3((normal)); // Get light - Light light = getLight(); + Light light = getKeyLight(); vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin float metallic = 0.0; @@ -65,7 +65,7 @@ vec3 drawScatteringTableUV(vec2 cursor, vec2 texcoord) { float curvature = unpackCurvature(diffusedCurvature.w); // Get light - Light light = getLight(); + Light light = getKeyLight(); vec3 fresnel = vec3(0.028); // Default Di-electric fresnel value for skin vec3 fragLightDir = -normalize(getLightDirection(light)); diff --git a/libraries/render-utils/src/zone_drawKeyLight.slf b/libraries/render-utils/src/zone_drawKeyLight.slf index ea11c775b4..5f0f0265d3 100644 --- a/libraries/render-utils/src/zone_drawKeyLight.slf +++ b/libraries/render-utils/src/zone_drawKeyLight.slf @@ -25,7 +25,7 @@ void main(void) { <$evalGlobeWidget()$> - Light light = getLight(); + Light light = getKeyLight(); vec3 lightDirection = normalize(getLightDirection(light)); vec3 lightIrradiance = getLightIrradiance(light); vec3 color = vec3(0.0); diff --git a/libraries/render/src/render/Engine.cpp b/libraries/render/src/render/Engine.cpp index 5f67d40d17..463b45451b 100644 --- a/libraries/render/src/render/Engine.cpp +++ b/libraries/render/src/render/Engine.cpp @@ -45,8 +45,8 @@ void Engine::load() { auto config = getConfiguration(); const QString configFile= "config/render.json"; - QUrl path(PathUtils::resourcesPath() + configFile); - QFile file(path.toString()); + QString path(PathUtils::resourcesPath() + configFile); + QFile file(path); if (!file.exists()) { qWarning() << "Engine configuration file" << path << "does not exist"; } else if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index eb279f18af..fb58c21dde 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -70,6 +70,8 @@ void ShapePlumber::addPipeline(const Key& key, const gpu::ShaderPointer& program void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, BatchSetter batchSetter, ItemSetter itemSetter) { + ShapeKey key{ filter._flags }; + gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), Slot::BUFFER::LIGHTING_MODEL)); slotBindings.insert(gpu::Shader::Binding(std::string("skinClusterBuffer"), Slot::BUFFER::SKINNING)); @@ -82,6 +84,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p slotBindings.insert(gpu::Shader::Binding(std::string("emissiveMap"), Slot::MAP::EMISSIVE_LIGHTMAP)); slotBindings.insert(gpu::Shader::Binding(std::string("occlusionMap"), Slot::MAP::OCCLUSION)); slotBindings.insert(gpu::Shader::Binding(std::string("scatteringMap"), Slot::MAP::SCATTERING)); + slotBindings.insert(gpu::Shader::Binding(std::string("keyLightBuffer"), Slot::BUFFER::KEY_LIGHT)); slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), Slot::BUFFER::LIGHT)); slotBindings.insert(gpu::Shader::Binding(std::string("lightAmbientBuffer"), Slot::BUFFER::LIGHT_AMBIENT_BUFFER)); slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), Slot::MAP::LIGHT_AMBIENT)); @@ -89,6 +92,12 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p slotBindings.insert(gpu::Shader::Binding(std::string("fadeParametersBuffer"), Slot::BUFFER::FADE_PARAMETERS)); slotBindings.insert(gpu::Shader::Binding(std::string("hazeBuffer"), Slot::BUFFER::HAZE_MODEL)); + if (key.isTranslucent()) { + slotBindings.insert(gpu::Shader::Binding(std::string("clusterGridBuffer"), Slot::BUFFER::LIGHT_CLUSTER_GRID_CLUSTER_GRID_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("clusterContentBuffer"), Slot::BUFFER::LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("frustumGridBuffer"), Slot::BUFFER::LIGHT_CLUSTER_GRID_FRUSTUM_GRID_SLOT)); + } + gpu::Shader::makeProgram(*program, slotBindings); auto locations = std::make_shared(); @@ -103,14 +112,23 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p locations->skinClusterBufferUnit = program->getUniformBuffers().findLocation("skinClusterBuffer"); locations->materialBufferUnit = program->getUniformBuffers().findLocation("materialBuffer"); locations->texMapArrayBufferUnit = program->getUniformBuffers().findLocation("texMapArrayBuffer"); + locations->keyLightBufferUnit = program->getUniformBuffers().findLocation("keyLightBuffer"); locations->lightBufferUnit = program->getUniformBuffers().findLocation("lightBuffer"); locations->lightAmbientBufferUnit = program->getUniformBuffers().findLocation("lightAmbientBuffer"); locations->lightAmbientMapUnit = program->getTextures().findLocation("skyboxMap"); locations->fadeMaskTextureUnit = program->getTextures().findLocation("fadeMaskMap"); locations->fadeParameterBufferUnit = program->getUniformBuffers().findLocation("fadeParametersBuffer"); locations->hazeParameterBufferUnit = program->getUniformBuffers().findLocation("hazeParametersBuffer"); + if (key.isTranslucent()) { + locations->lightClusterGridBufferUnit = program->getUniformBuffers().findLocation("clusterGridBuffer"); + locations->lightClusterContentBufferUnit = program->getUniformBuffers().findLocation("clusterContentBuffer"); + locations->lightClusterFrustumBufferUnit = program->getUniformBuffers().findLocation("frustumGridBuffer"); + } else { + locations->lightClusterGridBufferUnit = -1; + locations->lightClusterContentBufferUnit = -1; + locations->lightClusterFrustumBufferUnit = -1; + } - ShapeKey key{filter._flags}; auto gpuPipeline = gpu::Pipeline::create(program, state); auto shapePipeline = std::make_shared(gpuPipeline, locations, batchSetter, itemSetter); addPipelineHelper(filter, key, 0, shapePipeline); diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 9e05cfb81b..1dd9f5da49 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -235,10 +235,15 @@ public: MATERIAL, TEXMAPARRAY, LIGHTING_MODEL, + KEY_LIGHT, LIGHT, LIGHT_AMBIENT_BUFFER, HAZE_MODEL, FADE_PARAMETERS, + LIGHT_CLUSTER_GRID_FRUSTUM_GRID_SLOT, + LIGHT_CLUSTER_GRID_CLUSTER_GRID_SLOT, + LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT, + }; enum MAP { @@ -266,12 +271,16 @@ public: int skinClusterBufferUnit; int materialBufferUnit; int texMapArrayBufferUnit; + int keyLightBufferUnit; int lightBufferUnit; int lightAmbientBufferUnit; int lightAmbientMapUnit; int fadeMaskTextureUnit; int fadeParameterBufferUnit; int hazeParameterBufferUnit; + int lightClusterGridBufferUnit; + int lightClusterContentBufferUnit; + int lightClusterFrustumBufferUnit; }; using LocationsPointer = std::shared_ptr; diff --git a/libraries/script-engine/src/ArrayBufferClass.cpp b/libraries/script-engine/src/ArrayBufferClass.cpp index b84188f707..4a06dee391 100644 --- a/libraries/script-engine/src/ArrayBufferClass.cpp +++ b/libraries/script-engine/src/ArrayBufferClass.cpp @@ -18,14 +18,19 @@ #include "ArrayBufferClass.h" + static const QString CLASS_NAME = "ArrayBuffer"; +// FIXME: Q_DECLARE_METATYPE is global and really belongs in a shared header file, not per .cpp like this +// (see DataViewClass.cpp, etc. which would also have to be updated to resolve) +Q_DECLARE_METATYPE(QScriptClass*) Q_DECLARE_METATYPE(QByteArray*) +int qScriptClassPointerMetaTypeId = qRegisterMetaType(); +int qByteArrayPointerMetaTypeId = qRegisterMetaType(); ArrayBufferClass::ArrayBufferClass(ScriptEngine* scriptEngine) : QObject(scriptEngine), -QScriptClass(scriptEngine), -_scriptEngine(scriptEngine) { +QScriptClass(scriptEngine) { qScriptRegisterMetaType(engine(), toScriptValue, fromScriptValue); QScriptValue global = engine()->globalObject(); @@ -144,12 +149,30 @@ QScriptValue ArrayBufferClass::toScriptValue(QScriptEngine* engine, const QByteA QScriptValue ctor = engine->globalObject().property(CLASS_NAME); ArrayBufferClass* cls = qscriptvalue_cast(ctor.data()); if (!cls) { - return engine->newVariant(QVariant::fromValue(ba)); + if (engine->currentContext()) { + engine->currentContext()->throwError("arrayBufferClass::toScriptValue -- could not get " + CLASS_NAME + " class constructor"); + } + return QScriptValue::NullValue; } return cls->newInstance(ba); } -void ArrayBufferClass::fromScriptValue(const QScriptValue& obj, QByteArray& ba) { - ba = qvariant_cast(obj.data().toVariant()); +void ArrayBufferClass::fromScriptValue(const QScriptValue& object, QByteArray& byteArray) { + if (object.isString()) { + // UTF-8 encoded String + byteArray = object.toString().toUtf8(); + } else if (object.isArray()) { + // Array of uint8s eg: [ 128, 3, 25, 234 ] + auto Uint8Array = object.engine()->globalObject().property("Uint8Array"); + auto typedArray = Uint8Array.construct(QScriptValueList{object}); + if (QByteArray* buffer = qscriptvalue_cast(typedArray.property("buffer"))) { + byteArray = *buffer; + } + } else if (object.isObject()) { + // ArrayBuffer instance (or any JS class that supports coercion into QByteArray*) + if (QByteArray* buffer = qscriptvalue_cast(object.data())) { + byteArray = *buffer; + } + } } diff --git a/libraries/script-engine/src/ArrayBufferClass.h b/libraries/script-engine/src/ArrayBufferClass.h index 166a21b773..295b9c3fa4 100644 --- a/libraries/script-engine/src/ArrayBufferClass.h +++ b/libraries/script-engine/src/ArrayBufferClass.h @@ -41,7 +41,6 @@ public: QString name() const override; QScriptValue prototype() const override; - ScriptEngine* getEngine() { return _scriptEngine; } private: static QScriptValue construct(QScriptContext* context, QScriptEngine* engine); @@ -56,7 +55,6 @@ private: QScriptString _name; QScriptString _byteLength; - ScriptEngine* _scriptEngine; }; #endif // hifi_ArrayBufferClass_h diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index e0e04a1e25..1c811573fb 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -11,102 +11,113 @@ #include "AssetScriptingInterface.h" +#include +#include #include #include #include +#include +#include #include #include +#include +#include "ScriptEngine.h" #include "ScriptEngineLogging.h" +#include +#include -AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) : - _engine(engine) -{ +using Promise = MiniPromise::Promise; + +AssetScriptingInterface::AssetScriptingInterface(QObject* parent) : BaseAssetScriptingInterface(parent) { + qCDebug(scriptengine) << "AssetScriptingInterface::AssetScriptingInterface" << parent; + MiniPromise::registerMetaTypes(parent); } +#define JS_VERIFY(cond, error) { if (!this->jsVerify(cond, error)) { return; } } + void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) { + auto handler = jsBindCallback(thisObject(), callback); QByteArray dataByteArray = data.toUtf8(); auto upload = DependencyManager::get()->createUpload(dataByteArray); - QObject::connect(upload, &AssetUpload::finished, this, [this, callback](AssetUpload* upload, const QString& hash) mutable { - if (callback.isFunction()) { - QString url = "atp:" + hash; - QScriptValueList args { url, hash }; - callback.call(_engine->currentContext()->thisObject(), args); - } + Promise deferred = makePromise(__FUNCTION__); + deferred->ready([=](QString error, QVariantMap result) { + auto url = result.value("url").toString(); + auto hash = result.value("hash").toString(); + jsCallback(handler, url, hash); + }); + + connect(upload, &AssetUpload::finished, upload, [this, deferred](AssetUpload* upload, const QString& hash) { + // we are now on the "Resource Manager" thread (and "hash" being a *reference* makes it unsafe to use directly) + Q_ASSERT(QThread::currentThread() == upload->thread()); + deferred->resolve({ + { "url", "atp:" + hash }, + { "hash", hash }, + }); upload->deleteLater(); }); upload->start(); } void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValue callback) { - auto setMappingRequest = DependencyManager::get()->createSetMappingRequest(path, hash); + auto handler = jsBindCallback(thisObject(), callback); + auto setMappingRequest = assetClient()->createSetMappingRequest(path, hash); + Promise deferred = makePromise(__FUNCTION__); + deferred->ready([=](QString error, QVariantMap result) { + jsCallback(handler, error, result); + }); - QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable { - if (callback.isFunction()) { - QString error = request->getErrorString(); - QScriptValueList args { error }; - callback.call(_engine->currentContext()->thisObject(), args); - } + connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [this, deferred](SetMappingRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread + QString error = request->getErrorString(); + // forward a thread-safe values back to our thread + deferred->handle(error, { { "error", request->getError() } }); request->deleteLater(); }); setMappingRequest->start(); } -void AssetScriptingInterface::getMapping(QString path, QScriptValue callback) { - auto request = DependencyManager::get()->createGetMappingRequest(path); - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - auto result = request->getError(); - if (callback.isFunction()) { - if (result == GetMappingRequest::NotFound) { - QScriptValueList args { "", true }; - callback.call(_engine->currentContext()->thisObject(), args); - } else if (result == GetMappingRequest::NoError) { - QScriptValueList args { request->getHash(), true }; - callback.call(_engine->currentContext()->thisObject(), args); - } else { - qCDebug(scriptengine) << "error -- " << request->getError() << " -- " << request->getErrorString(); - QScriptValueList args { "", false }; - callback.call(_engine->currentContext()->thisObject(), args); - } - request->deleteLater(); - } - }); - request->start(); -} - void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { + // FIXME: historically this API method failed silently when given a non-atp prefixed + // urlString (or if the AssetRequest failed). + // .. is that by design or could we update without breaking things to provide better feedback to scripts? if (!urlString.startsWith(ATP_SCHEME)) { + // ... for now at least log a message so user can check logs qCDebug(scriptengine) << "AssetScriptingInterface::downloadData url must be of form atp:"; return; } - - // Make request to atp - auto path = urlString.right(urlString.length() - ATP_SCHEME.length()); - auto parts = path.split(".", QString::SkipEmptyParts); - auto hash = parts.length() > 0 ? parts[0] : ""; - + QString hash = AssetUtils::extractAssetHash(urlString); + auto handler = jsBindCallback(thisObject(), callback); auto assetClient = DependencyManager::get(); auto assetRequest = assetClient->createRequest(hash); - _pendingRequests << assetRequest; + Promise deferred = makePromise(__FUNCTION__); + deferred->ready([=](QString error, QVariantMap result) { + // FIXME: to remain backwards-compatible the signature here is "callback(data, n/a)" + jsCallback(handler, result.value("data").toString(), { { "errorMessage", error } }); + }); - connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable { + connect(assetRequest, &AssetRequest::finished, assetRequest, [this, deferred](AssetRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread Q_ASSERT(request->getState() == AssetRequest::Finished); if (request->getError() == AssetRequest::Error::NoError) { - if (callback.isFunction()) { - QString data = QString::fromUtf8(request->getData()); - QScriptValueList args { data }; - callback.call(_engine->currentContext()->thisObject(), args); - } + QString data = QString::fromUtf8(request->getData()); + // forward a thread-safe values back to our thread + deferred->resolve({ { "data", data } }); + } else { + // FIXME: propagate error to scripts? (requires changing signature or inverting param order above..) + //deferred->resolve(request->getErrorString(), { { "error", requet->getError() } }); + qCDebug(scriptengine) << "AssetScriptingInterface::downloadData ERROR: " << request->getErrorString(); } request->deleteLater(); - _pendingRequests.remove(request); }); assetRequest->start(); @@ -115,12 +126,15 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) { auto setBakingEnabledRequest = DependencyManager::get()->createSetBakingEnabledRequest({ path }, enabled); - QObject::connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable { - if (callback.isFunction()) { - QString error = request->getErrorString(); - QScriptValueList args{ error }; - callback.call(_engine->currentContext()->thisObject(), args); - } + Promise deferred = jsPromiseReady(makePromise(__FUNCTION__), thisObject(), callback); + + connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, setBakingEnabledRequest, [this, deferred](SetBakingEnabledRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread + + QString error = request->getErrorString(); + // forward thread-safe values back to our thread + deferred->handle(error, {}); request->deleteLater(); }); setBakingEnabledRequest->start(); @@ -135,3 +149,298 @@ void AssetScriptingInterface::sendFakedHandshake() { } #endif + +void AssetScriptingInterface::getMapping(QString asset, QScriptValue callback) { + auto path = AssetUtils::getATPUrl(asset).path(); + auto handler = jsBindCallback(thisObject(), callback); + JS_VERIFY(AssetUtils::isValidFilePath(path), "invalid ATP file path: " + asset + "(path:"+path+")"); + JS_VERIFY(callback.isFunction(), "expected second parameter to be a callback function"); + Promise promise = getAssetInfo(path); + promise->ready([=](QString error, QVariantMap result) { + jsCallback(handler, error, result.value("hash").toString()); + }); +} + +bool AssetScriptingInterface::jsVerify(bool condition, const QString& error) { + if (condition) { + return true; + } + if (context()) { + context()->throwError(error); + } else { + qCDebug(scriptengine) << "WARNING -- jsVerify failed outside of a valid JS context: " + error; + } + return false; +} + +QScriptValue AssetScriptingInterface::jsBindCallback(QScriptValue scope, QScriptValue callback) { + QScriptValue handler = ::makeScopedHandlerObject(scope, callback); + QScriptValue value = handler.property("callback"); + if (!jsVerify(handler.isObject() && value.isFunction(), + QString("jsBindCallback -- .callback is not a function (%1)").arg(value.toVariant().typeName()))) { + return QScriptValue(); + } + return handler; +} + +Promise AssetScriptingInterface::jsPromiseReady(Promise promise, QScriptValue scope, QScriptValue callback) { + auto handler = jsBindCallback(scope, callback); + if (!jsVerify(handler.isValid(), "jsPromiseReady -- invalid callback handler")) { + return nullptr; + } + return promise->ready([this, handler](QString error, QVariantMap result) { + jsCallback(handler, error, result); + }); +} + +void AssetScriptingInterface::jsCallback(const QScriptValue& handler, + const QScriptValue& error, const QScriptValue& result) { + Q_ASSERT(thread() == QThread::currentThread()); + auto errorValue = !error.toBool() ? QScriptValue::NullValue : error; + JS_VERIFY(handler.isObject() && handler.property("callback").isFunction(), + QString("jsCallback -- .callback is not a function (%1)") + .arg(handler.property("callback").toVariant().typeName())); + ::callScopedHandlerObject(handler, errorValue, result); +} + +void AssetScriptingInterface::jsCallback(const QScriptValue& handler, + const QScriptValue& error, const QVariantMap& result) { + Q_ASSERT(thread() == QThread::currentThread()); + Q_ASSERT(handler.engine()); + auto engine = handler.engine(); + jsCallback(handler, error, engine->toScriptValue(result)); +} + +void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + jsVerify(false, "TODO: deleteAsset API"); +} + +/**jsdoc + * @typedef {string} Assets.GetOptions.ResponseType + *

Available responseType values for use with @{link Assets.getAsset} and @{link Assets.loadFromCache} configuration option.

+ * + * + * + * + * + * + * + * + * + *
responseTypetypeof response value
"text"contents returned as utf-8 decoded String value
"arraybuffer"contents as a binary ArrayBuffer object
"json"contents as a parsed JSON object
+ */ + +void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + JS_VERIFY(options.isObject() || options.isString(), "expected request options Object or URL as first parameter"); + + auto decompress = options.property("decompress").toBool() || options.property("compressed").toBool(); + auto responseType = options.property("responseType").toString().toLower(); + auto url = options.property("url").toString(); + if (options.isString()) { + url = options.toString(); + } + if (responseType.isEmpty()) { + responseType = "text"; + } + auto asset = AssetUtils::getATPUrl(url).path(); + JS_VERIFY(AssetUtils::isValidHash(asset) || AssetUtils::isValidFilePath(asset), + QString("Invalid ATP url '%1'").arg(url)); + JS_VERIFY(RESPONSE_TYPES.contains(responseType), + QString("Invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | "))); + + Promise fetched = jsPromiseReady(makePromise("fetched"), scope, callback); + Promise mapped = makePromise("mapped"); + + mapped->fail(fetched); + mapped->then([=](QVariantMap result) { + QString hash = result.value("hash").toString(); + QString url = result.value("url").toString(); + if (!AssetUtils::isValidHash(hash)) { + fetched->reject("internal hash error: " + hash, result); + } else { + Promise promise = loadAsset(hash, decompress, responseType); + promise->mixin(result); + promise->ready([=](QString error, QVariantMap loadResult) { + loadResult["url"] = url; // maintain mapped .url in results (vs. atp:hash returned by loadAsset) + fetched->handle(error, loadResult); + }); + } + }); + + if (AssetUtils::isValidHash(asset)) { + mapped->resolve({ + { "hash", asset }, + { "url", url }, + }); + } else { + getAssetInfo(asset)->ready(mapped); + } +} + +void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + const QString& URL{ "url" }; + + auto url = (options.isString() ? options : options.property(URL)).toString(); + auto asset = AssetUtils::getATPUrl(url).path(); + + JS_VERIFY(AssetUtils::isValidFilePath(asset) || AssetUtils::isValidHash(asset), + "expected options to be an asset URL or request options containing .url property"); + + jsPromiseReady(getAssetInfo(asset), scope, callback); +} + +void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { + auto data = options.property("data"); + QByteArray dataByteArray = qscriptvalue_cast(data); + auto responseType = options.property("responseType").toString().toLower(); + if (responseType.isEmpty()) { + responseType = "text"; + } + Promise completed = jsPromiseReady(makePromise(__FUNCTION__), scope, callback); + Promise decompressed = decompressBytes(dataByteArray); + if (responseType == "arraybuffer") { + decompressed->ready(completed); + } else { + decompressed->ready([=](QString error, QVariantMap result) { + Promise converted = convertBytes(result.value("data").toByteArray(), responseType); + converted->mixin(result); + converted->ready(completed); + }); + } +} + +namespace { + const int32_t DEFAULT_GZIP_COMPRESSION_LEVEL = -1; + const int32_t MAX_GZIP_COMPRESSION_LEVEL = 9; +} +void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { + auto data = options.property("data").isValid() ? options.property("data") : options; + QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast(data); + int level = options.property("level").isNumber() ? options.property("level").toInt32() : DEFAULT_GZIP_COMPRESSION_LEVEL; + JS_VERIFY(level >= DEFAULT_GZIP_COMPRESSION_LEVEL || level <= MAX_GZIP_COMPRESSION_LEVEL, QString("invalid .level %1").arg(level)); + jsPromiseReady(compressBytes(dataByteArray, level), scope, callback); +} + +void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + auto compress = options.property("compress").toBool() || options.property("compressed").toBool(); + auto data = options.isObject() ? options.property("data") : options; + auto rawPath = options.property("path").toString(); + auto path = AssetUtils::getATPUrl(rawPath).path(); + + QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast(data); + + JS_VERIFY(path.isEmpty() || AssetUtils::isValidFilePath(path), + QString("expected valid ATP file path '%1' ('%2')").arg(rawPath).arg(path)); + JS_VERIFY(dataByteArray.size() > 0, + QString("expected non-zero .data (got %1 / #%2 bytes)").arg(data.toVariant().typeName()).arg(dataByteArray.size())); + + // [compressed] => uploaded to server => [mapped to path] + Promise prepared = makePromise("putAsset::prepared"); + Promise uploaded = makePromise("putAsset::uploaded"); + Promise completed = makePromise("putAsset::completed"); + jsPromiseReady(completed, scope, callback); + + if (compress) { + Promise compress = compressBytes(dataByteArray, DEFAULT_GZIP_COMPRESSION_LEVEL); + compress->ready(prepared); + } else { + prepared->resolve({{ "data", dataByteArray }}); + } + + prepared->fail(completed); + prepared->then([=](QVariantMap result) { + Promise upload = uploadBytes(result.value("data").toByteArray()); + upload->mixin(result); + upload->ready(uploaded); + }); + + uploaded->fail(completed); + if (path.isEmpty()) { + uploaded->then(completed); + } else { + uploaded->then([=](QVariantMap result) { + QString hash = result.value("hash").toString(); + if (!AssetUtils::isValidHash(hash)) { + completed->reject("path mapping requested, but did not receive valid hash", result); + } else { + Promise link = symlinkAsset(hash, path); + link->mixin(result); + link->ready(completed); + } + }); + } +} + +void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback) { + QString url = options.isString() ? options.toString() : options.property("url").toString(); + JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url)); + jsPromiseReady(Parent::queryCacheMeta(url), scope, callback); +} + +void AssetScriptingInterface::loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { + QString url, responseType; + bool decompress = false; + if (options.isString()) { + url = options.toString(); + responseType = "text"; + } else { + url = options.property("url").toString(); + responseType = options.property("responseType").isValid() ? options.property("responseType").toString() : "text"; + decompress = options.property("decompress").toBool() || options.property("compressed").toBool(); + } + JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url)); + JS_VERIFY(RESPONSE_TYPES.contains(responseType), + QString("Invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | "))); + + jsPromiseReady(Parent::loadFromCache(url, decompress, responseType), scope, callback); +} + +bool AssetScriptingInterface::canWriteCacheValue(const QUrl& url) { + auto scriptEngine = qobject_cast(engine()); + if (!scriptEngine) { + qCDebug(scriptengine) << __FUNCTION__ << "invalid script engine" << url; + return false; + } + // allow cache writes only from Client, EntityServer and Agent scripts + bool isAllowedContext = ( + scriptEngine->isClientScript() || + scriptEngine->isAgentScript() + ); + if (!isAllowedContext) { + qCDebug(scriptengine) << __FUNCTION__ << "invalid context" << scriptEngine->getContext() << url; + return false; + } + return true; +} + +void AssetScriptingInterface::saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { + JS_VERIFY(options.isObject(), QString("expected options object as first parameter not: %1").arg(options.toVariant().typeName())); + + QString url = options.property("url").toString(); + QByteArray data = qscriptvalue_cast(options.property("data")); + QVariantMap headers = qscriptvalue_cast(options.property("headers")); + + saveToCache(url, data, headers, scope, callback); +} + +void AssetScriptingInterface::saveToCache(const QUrl& rawURL, const QByteArray& data, const QVariantMap& metadata, QScriptValue scope, QScriptValue callback) { + QUrl url = rawURL; + if (url.path().isEmpty() && !data.isEmpty()) { + // generate a valid ATP URL from the data -- appending any existing fragment or querystring values + auto atpURL = AssetUtils::getATPUrl(hashDataHex(data)); + atpURL.setQuery(url.query()); + atpURL.setFragment(url.fragment()); + qCDebug(scriptengine) << "autogenerated ATP URL" << url << "=>" << atpURL; + url = atpURL; + } + auto hash = AssetUtils::extractAssetHash(url.toDisplayString()); + + JS_VERIFY(url.isValid(), QString("Invalid URL '%1'").arg(url.toString())); + JS_VERIFY(canWriteCacheValue(url), "Invalid cache write URL: " + url.toString()); + JS_VERIFY(url.scheme() == "atp" || url.scheme() == "cache", "only 'atp' and 'cache' URL schemes supported"); + JS_VERIFY(hash.isEmpty() || hash == hashDataHex(data), QString("invalid checksum hash for atp:HASH style URL (%1 != %2)").arg(hash, hashDataHex(data))); + + qCDebug(scriptengine) << "saveToCache" << url.toDisplayString() << data << hash << metadata; + + jsPromiseReady(Parent::saveToCache(url, data, metadata), scope, callback); +} diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index dded2ef21d..fdce173dfe 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -15,17 +15,23 @@ #define hifi_AssetScriptingInterface_h #include +#include #include - +#include #include +#include +#include +#include +#include /**jsdoc * @namespace Assets */ -class AssetScriptingInterface : public QObject { +class AssetScriptingInterface : public BaseAssetScriptingInterface, QScriptable { Q_OBJECT public: - AssetScriptingInterface(QScriptEngine* engine); + using Parent = BaseAssetScriptingInterface; + AssetScriptingInterface(QObject* parent = nullptr); /**jsdoc * Upload content to the connected domain's asset server. @@ -87,21 +93,105 @@ public: /**jsdoc * Called when getMapping is complete. * @callback Assets~getMappingCallback + * @param error {string} error description if the path could not be resolved; otherwise a null value. * @param assetID {string} hash value if found, else an empty string - * @param success {boolean} false for errors other than "not found", else true */ Q_INVOKABLE void getMapping(QString path, QScriptValue callback); - Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback); #if (PR_BUILD || DEV_BUILD) Q_INVOKABLE void sendFakedHandshake(); #endif + /**jsdoc + * Request Asset data from the ATP Server + * @function Assets.getAsset + * @param {URL|Assets.GetOptions} options An atp: style URL, hash, or relative mapped path; or an {@link Assets.GetOptions} object with request parameters + * @param {Assets~getAssetCallback} scope[callback] A scope callback function to receive (error, results) values + */ + /**jsdoc + * A set of properties that can be passed to {@link Assets.getAsset}. + * @typedef {Object} Assets.GetOptions + * @property {URL} [url] an "atp:" style URL, hash, or relative mapped path to fetch + * @property {string} [responseType=text] the desired reponse type (text | arraybuffer | json) + * @property {boolean} [decompress=false] whether to attempt gunzip decompression on the fetched data + * See: {@link Assets.putAsset} and its .compress=true option + */ + /**jsdoc + * Called when Assets.getAsset is complete. + * @callback Assets~getAssetCallback + * @param {string} error - contains error message or null value if no error occured fetching the asset + * @param {Asset~getAssetResult} result - result object containing, on success containing asset metadata and contents + */ + /**jsdoc + * Result value returned by {@link Assets.getAsset}. + * @typedef {Object} Assets~getAssetResult + * @property {url} [url] the resolved "atp:" style URL for the fetched asset + * @property {string} [hash] the resolved hash for the fetched asset + * @property {string|ArrayBuffer|Object} [response] response data (possibly converted per .responseType value) + * @property {string} [responseType] response type (text | arraybuffer | json) + * @property {string} [contentType] detected asset mime-type (autodetected) + * @property {number} [byteLength] response data size in bytes + * @property {number} [decompressed] flag indicating whether data was decompressed + */ + Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + + /**jsdoc + * Upload Asset data to the ATP Server + * @function Assets.putAsset + * @param {Assets.PutOptions} options A PutOptions object with upload parameters + * @param {Assets~putAssetCallback} scope[callback] A scoped callback function invoked with (error, results) + */ + /**jsdoc + * A set of properties that can be passed to {@link Assets.putAsset}. + * @typedef {Object} Assets.PutOptions + * @property {ArrayBuffer|string} [data] byte buffer or string value representing the new asset's content + * @property {string} [path=null] ATP path mapping to automatically create (upon successful upload to hash) + * @property {boolean} [compress=false] whether to gzip compress data before uploading + */ + /**jsdoc + * Called when Assets.putAsset is complete. + * @callback Assets~puttAssetCallback + * @param {string} error - contains error message (or null value if no error occured while uploading/mapping the new asset) + * @param {Asset~putAssetResult} result - result object containing error or result status of asset upload + */ + /**jsdoc + * Result value returned by {@link Assets.putAsset}. + * @typedef {Object} Assets~putAssetResult + * @property {url} [url] the resolved "atp:" style URL for the uploaded asset (based on .path if specified, otherwise on the resulting ATP hash) + * @property {string} [path] the uploaded asset's resulting ATP path (or undefined if no path mapping was assigned) + * @property {string} [hash] the uploaded asset's resulting ATP hash + * @property {boolean} [compressed] flag indicating whether the data was compressed before upload + * @property {number} [byteLength] flag indicating final byte size of the data uploaded to the ATP server + */ + Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + + Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + + Q_INVOKABLE bool initializeCache() { return Parent::initializeCache(); } + + Q_INVOKABLE bool canWriteCacheValue(const QUrl& url); + + Q_INVOKABLE void getCacheStatus(QScriptValue scope, QScriptValue callback = QScriptValue()) { + jsPromiseReady(Parent::getCacheStatus(), scope, callback); + } + + Q_INVOKABLE void queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata, + QScriptValue scope, QScriptValue callback = QScriptValue()); protected: - QSet _pendingRequests; - QScriptEngine* _engine; + QScriptValue jsBindCallback(QScriptValue scope, QScriptValue callback = QScriptValue()); + Promise jsPromiseReady(Promise promise, QScriptValue scope, QScriptValue callback = QScriptValue()); + + void jsCallback(const QScriptValue& handler, const QScriptValue& error, const QVariantMap& result); + void jsCallback(const QScriptValue& handler, const QScriptValue& error, const QScriptValue& result); + bool jsVerify(bool condition, const QString& error); }; #endif // hifi_AssetScriptingInterface_h diff --git a/libraries/script-engine/src/Mat4.cpp b/libraries/script-engine/src/Mat4.cpp index 6965f43b32..15015782e2 100644 --- a/libraries/script-engine/src/Mat4.cpp +++ b/libraries/script-engine/src/Mat4.cpp @@ -11,7 +11,9 @@ #include #include +#include #include +#include #include "ScriptEngineLogging.h" #include "ScriptEngine.h" #include "Mat4.h" @@ -32,6 +34,14 @@ glm::mat4 Mat4::createFromColumns(const glm::vec4& col0, const glm::vec4& col1, return glm::mat4(col0, col1, col2, col3); } +glm::mat4 Mat4::createFromArray(const QVector& floats) const { + if (floats.size() != 16 && floats.size() != 9) { + context()->throwError("createFromVector requires 16 floats for mat4 (or 9 if providing a mat3)"); + return glm::mat4(); + } + return floats.size() == 9 ? glm::mat4(glm::make_mat3(floats.constData())) : glm::make_mat4(floats.constData()); +} + glm::vec3 Mat4::extractTranslation(const glm::mat4& m) const { return ::extractTranslation(m); } diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 8b942874ee..ceeea3ccec 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include "RegisteredMetaTypes.h" /// Scriptable Mat4 object. Used exclusively in the JavaScript API class Mat4 : public QObject, protected QScriptable { @@ -28,6 +31,7 @@ public slots: glm::mat4 createFromRotAndTrans(const glm::quat& rot, const glm::vec3& trans) const; glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans) const; glm::mat4 createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; + glm::mat4 createFromArray(const QVector& floats) const; glm::vec3 extractTranslation(const glm::mat4& m) const; glm::quat extractRotation(const glm::mat4& m) const; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 9d073453ae..c79ffffec7 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -56,6 +56,7 @@ #include #include "ArrayBufferViewClass.h" +#include "AssetScriptingInterface.h" #include "BatchLoader.h" #include "BaseScriptEngine.h" #include "DataViewClass.h" @@ -175,6 +176,7 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const _timerFunctionMap(), _fileNameString(fileNameString), _arrayBufferClass(new ArrayBufferClass(this)), + _assetScriptingInterface(new AssetScriptingInterface(this)), // don't delete `ScriptEngines` until all `ScriptEngine`s are gone _scriptEngines(DependencyManager::get()) { @@ -704,7 +706,7 @@ void ScriptEngine::init() { // constants globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); - registerGlobalObject("Assets", &_assetScriptingInterface); + registerGlobalObject("Assets", _assetScriptingInterface); registerGlobalObject("Resources", DependencyManager::get().data()); registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 17c0e0713a..7f69eee990 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -321,7 +321,7 @@ protected: ArrayBufferClass* _arrayBufferClass; - AssetScriptingInterface _assetScriptingInterface{ this }; + AssetScriptingInterface* _assetScriptingInterface; std::function _emitScriptUpdates{ []() { return true; } }; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 6cfa678652..78cb05fa0d 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -62,8 +62,7 @@ void ScriptEngines::onErrorLoadingScript(const QString& url) { } ScriptEngines::ScriptEngines(ScriptEngine::Context context) - : _context(context), - _scriptsLocationHandle("scriptsLocation", DESKTOP_LOCATION) + : _context(context) { _scriptsModelFilter.setSourceModel(&_scriptsModel); _scriptsModelFilter.sort(0, Qt::AscendingOrder); @@ -429,13 +428,8 @@ bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { return stoppedScript; } -QString ScriptEngines::getScriptsLocation() const { - return _scriptsLocationHandle.get(); -} - -void ScriptEngines::setScriptsLocation(const QString& scriptsLocation) { - _scriptsLocationHandle.set(scriptsLocation); - _scriptsModel.updateScriptsLocation(scriptsLocation); +void ScriptEngines::reloadLocalFiles() { + _scriptsModel.reloadLocalFiles(); } void ScriptEngines::reloadAllScripts() { diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 0f807b08cf..5a4b8f2f47 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -45,9 +45,9 @@ public: QString getDebugScriptUrl() { return _debugScriptUrl; }; void setDebugScriptUrl(const QString& url) { _debugScriptUrl = url; }; - QString getScriptsLocation() const; void loadDefaultScripts(); - void setScriptsLocation(const QString& scriptsLocation); + void reloadLocalFiles(); + QStringList getRunningScripts(); ScriptEnginePointer getScriptEngine(const QUrl& scriptHash); @@ -115,7 +115,6 @@ protected: QSet _allKnownScriptEngines; QMutex _allScriptsMutex; std::list _scriptInitializers; - mutable Setting::Handle _scriptsLocationHandle; ScriptsModel _scriptsModel; ScriptsModelFilter _scriptsModelFilter; std::atomic _isStopped { false }; diff --git a/libraries/script-engine/src/ScriptsModel.cpp b/libraries/script-engine/src/ScriptsModel.cpp index 9b7a367773..5f30033bf9 100644 --- a/libraries/script-engine/src/ScriptsModel.cpp +++ b/libraries/script-engine/src/ScriptsModel.cpp @@ -100,6 +100,7 @@ QVariant ScriptsModel::data(const QModelIndex& index, int role) const { return QVariant(); } if (node->getType() == TREE_NODE_TYPE_SCRIPT) { + TreeNodeScript* script = static_cast(node); if (role == Qt::DisplayRole) { return QVariant(script->getName() + (script->getOrigin() == SCRIPT_ORIGIN_LOCAL ? " (local)" : "")); @@ -133,7 +134,6 @@ void ScriptsModel::updateScriptsLocation(const QString& newPath) { _fsWatcher.addPath(_localDirectory.absolutePath()); } } - reloadLocalFiles(); } @@ -305,6 +305,7 @@ void ScriptsModel::rebuildTree() { _treeNodes.removeAt(i); } } + QHash folders; for (int i = 0; i < _treeNodes.size(); i++) { TreeNodeBase* node = _treeNodes.at(i); diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp index c92d629b75..22ae01d72f 100644 --- a/libraries/shared/src/BaseScriptEngine.cpp +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -325,6 +325,12 @@ QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue } else if (methodOrName.isFunction()) { scope = scopeOrCallback; callback = methodOrName; + } else if (!methodOrName.isValid()) { + // instantiate from an existing scoped handler object + if (scopeOrCallback.property("callback").isFunction()) { + scope = scopeOrCallback.property("scope"); + callback = scopeOrCallback.property("callback"); + } } } auto handler = engine->newObject(); diff --git a/libraries/shared/src/BitVectorHelpers.h b/libraries/shared/src/BitVectorHelpers.h index 71826036eb..9f5af0c380 100644 --- a/libraries/shared/src/BitVectorHelpers.h +++ b/libraries/shared/src/BitVectorHelpers.h @@ -12,14 +12,14 @@ #ifndef hifi_BitVectorHelpers_h #define hifi_BitVectorHelpers_h -size_t calcBitVectorSize(int numBits) { +int calcBitVectorSize(int numBits) { return ((numBits - 1) >> 3) + 1; } // func should be of type bool func(int index) template -size_t writeBitVector(uint8_t* destinationBuffer, int numBits, const F& func) { - size_t totalBytes = ((numBits - 1) >> 3) + 1; +int writeBitVector(uint8_t* destinationBuffer, int numBits, const F& func) { + int totalBytes = calcBitVectorSize(numBits); uint8_t* cursor = destinationBuffer; uint8_t byte = 0; uint8_t bit = 0; @@ -34,13 +34,19 @@ size_t writeBitVector(uint8_t* destinationBuffer, int numBits, const F& func) { bit = 0; } } + // write the last byte, if necessary + if (bit != 0) { + *cursor++ = byte; + } + + assert((int)(cursor - destinationBuffer) == totalBytes); return totalBytes; } // func should be of type 'void func(int index, bool value)' template -size_t readBitVector(const uint8_t* sourceBuffer, int numBits, const F& func) { - size_t totalBytes = ((numBits - 1) >> 3) + 1; +int readBitVector(const uint8_t* sourceBuffer, int numBits, const F& func) { + int totalBytes = calcBitVectorSize(numBits); const uint8_t* cursor = sourceBuffer; uint8_t bit = 0; diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index 956c61deaf..0742a5625b 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -292,6 +292,14 @@ glm::vec3 Triangle::getNormal() const { return glm::normalize(glm::cross(edge1, edge2)); } +Triangle Triangle::operator*(const glm::mat4& transform) const { + return { + glm::vec3(transform * glm::vec4(v0, 1.0f)), + glm::vec3(transform * glm::vec4(v1, 1.0f)), + glm::vec3(transform * glm::vec4(v2, 1.0f)) + }; +} + bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance, bool allowBackface) { glm::vec3 firstSide = v0 - v1; diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index dcb90643b6..4832616fbd 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -104,6 +104,7 @@ public: glm::vec3 v1; glm::vec3 v2; glm::vec3 getNormal() const; + Triangle operator*(const glm::mat4& transform) const; }; inline bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 35d7554f39..41db7281ac 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -29,7 +29,6 @@ #include #endif - #include "shared/GlobalAppProperties.h" #include "SharedUtil.h" @@ -41,8 +40,15 @@ QString TEMP_DIR_FORMAT { "%1-%2-%3" }; #if defined(Q_OS_OSX) static bool USE_SOURCE_TREE_RESOURCES = true; #else -static const QString USE_SOURCE_TREE_RESOURCES_FLAG("HIFI_USE_SOURCE_TREE_RESOURCES"); -static bool USE_SOURCE_TREE_RESOURCES = QProcessEnvironment::systemEnvironment().contains(USE_SOURCE_TREE_RESOURCES_FLAG); +static bool USE_SOURCE_TREE_RESOURCES() { + static bool result = false; + static std::once_flag once; + std::call_once(once, [&] { + const QString USE_SOURCE_TREE_RESOURCES_FLAG("HIFI_USE_SOURCE_TREE_RESOURCES"); + result = QProcessEnvironment::systemEnvironment().contains(USE_SOURCE_TREE_RESOURCES_FLAG); + }); + return result; +} #endif #endif @@ -77,7 +83,7 @@ const QString& PathUtils::resourcesPath() { #endif #if !defined(Q_OS_ANDROID) && defined(DEV_BUILD) - if (USE_SOURCE_TREE_RESOURCES) { + if (USE_SOURCE_TREE_RESOURCES()) { // For dev builds, optionally load content from the Git source tree staticResourcePath = projectRootPath() + "/interface/resources/"; } @@ -100,7 +106,7 @@ const QString& PathUtils::resourcesUrl() { #endif #if !defined(Q_OS_ANDROID) && defined(DEV_BUILD) - if (USE_SOURCE_TREE_RESOURCES) { + if (USE_SOURCE_TREE_RESOURCES()) { // For dev builds, optionally load content from the Git source tree staticResourcePath = QUrl::fromLocalFile(projectRootPath() + "/interface/resources/").toString(); } diff --git a/libraries/shared/src/TriangleSet.cpp b/libraries/shared/src/TriangleSet.cpp index ce7dd18921..3f8f748720 100644 --- a/libraries/shared/src/TriangleSet.cpp +++ b/libraries/shared/src/TriangleSet.cpp @@ -31,7 +31,7 @@ void TriangleSet::clear() { } bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface) { + float& distance, BoxFace& face, Triangle& triangle, bool precision, bool allowBackface) { // reset our distance to be the max possible, lower level tests will store best distance here distance = std::numeric_limits::max(); @@ -41,7 +41,7 @@ bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& } int trianglesTouched = 0; - auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched, allowBackface); + auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, triangle, precision, trianglesTouched, allowBackface); #if WANT_DEBUGGING if (precision) { @@ -95,11 +95,12 @@ void TriangleSet::balanceOctree() { // Determine of the given ray (origin/direction) in model space intersects with any triangles // in the set. If an intersection occurs, the distance and surface normal will be provided. bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) { + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, bool allowBackface) { bool intersectedSomething = false; float boxDistance = distance; float bestDistance = distance; + glm::vec3 surfaceNormal; if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) { @@ -112,14 +113,14 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec if (precision) { for (const auto& triangleIndex : _triangleIndices) { - const auto& triangle = _allTriangles[triangleIndex]; + const auto& thisTriangle = _allTriangles[triangleIndex]; float thisTriangleDistance; trianglesTouched++; - if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance, allowBackface)) { + if (findRayTriangleIntersection(origin, direction, thisTriangle, thisTriangleDistance, allowBackface)) { if (thisTriangleDistance < bestDistance) { bestDistance = thisTriangleDistance; intersectedSomething = true; - surfaceNormal = triangle.getNormal(); + triangle = thisTriangle; distance = bestDistance; } } @@ -204,7 +205,8 @@ void TriangleSet::TriangleOctreeCell::insert(size_t triangleIndex) { } bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) { + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, + bool allowBackface) { if (_population < 1) { return false; // no triangles below here, so we can't intersect @@ -212,6 +214,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi float bestLocalDistance = distance; BoxFace bestLocalFace; + Triangle bestLocalTriangle; glm::vec3 bestLocalNormal; bool intersects = false; @@ -229,7 +232,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi float childDistance = distance; BoxFace childFace; - glm::vec3 childNormal; + Triangle childTriangle; // if we're not yet at the max depth, then check which child the triangle fits in if (_depth < MAX_DEPTH) { @@ -237,22 +240,22 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi // check each child, if there's an intersection, it will return some distance that we need // to compare against the other results, because there might be multiple intersections and // we will always choose the best (shortest) intersection - if (child.second.findRayIntersection(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) { + if (child.second.findRayIntersection(origin, direction, childDistance, childFace, childTriangle, precision, trianglesTouched)) { if (childDistance < bestLocalDistance) { bestLocalDistance = childDistance; bestLocalFace = childFace; - bestLocalNormal = childNormal; + bestLocalTriangle = childTriangle; intersects = true; } } } } // also check our local triangle set - if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched, allowBackface)) { + if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childTriangle, precision, trianglesTouched, allowBackface)) { if (childDistance < bestLocalDistance) { bestLocalDistance = childDistance; bestLocalFace = childFace; - bestLocalNormal = childNormal; + bestLocalTriangle = childTriangle; intersects = true; } } @@ -260,7 +263,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi if (intersects) { distance = bestLocalDistance; face = bestLocalFace; - surfaceNormal = bestLocalNormal; + triangle = bestLocalTriangle; } return intersects; } diff --git a/libraries/shared/src/TriangleSet.h b/libraries/shared/src/TriangleSet.h index 3b0b33d7d5..786f58804f 100644 --- a/libraries/shared/src/TriangleSet.h +++ b/libraries/shared/src/TriangleSet.h @@ -27,7 +27,8 @@ class TriangleSet { void clear(); bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false); + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, + bool allowBackface = false); const AABox& getBounds() const { return _bounds; } @@ -38,7 +39,8 @@ class TriangleSet { // checks our internal list of triangles bool findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false); + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, + bool allowBackface = false); std::vector& _allTriangles; std::map _children; @@ -60,7 +62,7 @@ public: void insert(const Triangle& t); bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface = false); + float& distance, BoxFace& face, Triangle& triangle, bool precision, bool allowBackface = false); void balanceOctree(); @@ -72,7 +74,7 @@ public: // intersection occurs, the distance and surface normal will be provided. // note: this might side-effect internal structures bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched); + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched); // Determine if a point is "inside" all the triangles of a convex hull. It is the responsibility of the caller to // determine that the triangle set is indeed a convex hull. If the triangles added to this set are not in fact a diff --git a/libraries/shared/src/shared/MiniPromises.cpp b/libraries/shared/src/shared/MiniPromises.cpp new file mode 100644 index 0000000000..bb78852c29 --- /dev/null +++ b/libraries/shared/src/shared/MiniPromises.cpp @@ -0,0 +1,27 @@ +// +// Created by Timothy Dedischew on 2017/12/21 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "MiniPromises.h" +#include +#include + +int MiniPromise::metaTypeID = qRegisterMetaType("MiniPromise::Promise"); + +namespace { + void promiseFromScriptValue(const QScriptValue& object, MiniPromise::Promise& promise) { + Q_ASSERT(false); + } + QScriptValue promiseToScriptValue(QScriptEngine *engine, const MiniPromise::Promise& promise) { + return engine->newQObject(promise.get()); + } +} +void MiniPromise::registerMetaTypes(QObject* engine) { + auto scriptEngine = qobject_cast(engine); + qDebug() << "----------------------- MiniPromise::registerMetaTypes ------------" << scriptEngine; + qScriptRegisterMetaType(scriptEngine, promiseToScriptValue, promiseFromScriptValue); +} diff --git a/libraries/shared/src/shared/MiniPromises.h b/libraries/shared/src/shared/MiniPromises.h new file mode 100644 index 0000000000..5983f135b7 --- /dev/null +++ b/libraries/shared/src/shared/MiniPromises.h @@ -0,0 +1,278 @@ +// +// Created by Timothy Dedischew on 2017/12/21 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +// Minimalist threadsafe Promise-like helper for managing asynchronous results +// +// This class pivots around continuation-style callback handlers: +// auto successCallback = [=](QVariantMap result) { .... } +// auto errorCallback = [=](QString error) { .... } +// auto combinedCallback = [=](QString error, QVariantMap result) { .... } +// +// * Callback Handlers are automatically invoked on the right thread (the Promise's thread). +// * Callbacks can be assigned anytime during a Promise's life and "do the right thing". +// - ie: for code clarity you can define success cases first or choose to maintain time order +// * "Deferred" concept can be used to publish outcomes. +// * "Promise" concept be used to subscribe to outcomes. +// +// See AssetScriptingInterface.cpp for some examples of using to simplify chained async results. + +#include +#include +#include +#include +#include "ReadWriteLockable.h" +#include + +class MiniPromise : public QObject, public std::enable_shared_from_this, public ReadWriteLockable { + Q_OBJECT + Q_PROPERTY(QString state READ getStateString) + Q_PROPERTY(QString error READ getError) + Q_PROPERTY(QVariantMap result READ getResult) +public: + using HandlerFunction = std::function; + using SuccessFunction = std::function; + using ErrorFunction = std::function; + using HandlerFunctions = QVector; + using Promise = std::shared_ptr; + + static void registerMetaTypes(QObject* engine); + static int metaTypeID; + + MiniPromise() {} + MiniPromise(const QString debugName) { setObjectName(debugName); } + + ~MiniPromise() { + if (getStateString() == "pending") { + qWarning() << "MiniPromise::~MiniPromise -- destroying pending promise:" << objectName() << _error << _result << "handlers:" << getPendingHandlerCount(); + } + } + Promise self() { return shared_from_this(); } + + Q_INVOKABLE void executeOnPromiseThread(std::function function, MiniPromise::Promise root = nullptr) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod( + this, "executeOnPromiseThread", Qt::QueuedConnection, + Q_ARG(std::function, function), + Q_ARG(MiniPromise::Promise, self())); + } else { + function(); + } + } + + // result aggregation helpers -- eg: deferred->defaults(interimResultMap)->ready(...) + // copy values from the input map, but only for keys that don't already exist + Promise mixin(const QVariantMap& source) { + withWriteLock([&]{ + for (const auto& key : source.keys()) { + if (!_result.contains(key)) { + _result[key] = source[key]; + } + } + }); + return self(); + } + // copy values from the input map, replacing any existing keys + Promise assignResult(const QVariantMap& source) { + withWriteLock([&]{ + for (const auto& key : source.keys()) { + _result[key] = source[key]; + } + }); + return self(); + } + + // callback registration methods + Promise ready(HandlerFunction always) { return finally(always); } + Promise finally(HandlerFunction always) { + if (!_rejected && !_resolved) { + withWriteLock([&]{ + _onfinally << always; + }); + } else { + executeOnPromiseThread([&]{ + always(getError(), getResult()); + }); + } + return self(); + } + Promise fail(ErrorFunction errorOnly) { + return fail([this, errorOnly](QString error, QVariantMap result) { + errorOnly(error); + }); + } + + Promise fail(HandlerFunction failFunc) { + if (!_rejected) { + withWriteLock([&]{ + _onreject << failFunc; + }); + } else { + executeOnPromiseThread([&]{ + failFunc(getError(), getResult()); + }); + } + return self(); + } + + Promise then(SuccessFunction successOnly) { + return then([this, successOnly](QString error, QVariantMap result) { + successOnly(result); + }); + } + Promise then(HandlerFunction successFunc) { + if (!_resolved) { + withWriteLock([&]{ + _onresolve << successFunc; + }); + } else { + executeOnPromiseThread([&]{ + successFunc(getError(), getResult()); + }); + } + return self(); + } + + // NOTE: first arg may be null (see ES6 .then(null, errorHandler) conventions) + Promise then(SuccessFunction successOnly, ErrorFunction errorOnly) { + if (successOnly) { + then(successOnly); + } + if (errorOnly) { + fail(errorOnly); + } + return self(); + } + + + // helper functions for forwarding results on to a next Promise + Promise ready(Promise next) { return finally(next); } + Promise finally(Promise next) { + return finally([next](QString error, QVariantMap result) { + next->handle(error, result); + }); + } + Promise fail(Promise next) { + return fail([next](QString error, QVariantMap result) { + next->reject(error, result); + }); + } + Promise then(Promise next) { + return then([next](QString error, QVariantMap result) { + next->resolve(error, result); + }); + } + + + // trigger methods + // handle() automatically resolves or rejects the promise (based on whether an error value occurred) + Promise handle(QString error, const QVariantMap& result) { + if (error.isEmpty()) { + resolve(error, result); + } else { + reject(error, result); + } + return self(); + } + + Promise resolve(QVariantMap result) { + return resolve(QString(), result); + } + Promise resolve(QString error, const QVariantMap& result) { + setState(true, error, result); + + executeOnPromiseThread([&]{ + const QString localError{ getError() }; + const QVariantMap localResult{ getResult() }; + HandlerFunctions resolveHandlers; + HandlerFunctions finallyHandlers; + withReadLock([&]{ + resolveHandlers = _onresolve; + finallyHandlers = _onfinally; + }); + for (const auto& onresolve : resolveHandlers) { + onresolve(localError, localResult); + } + for (const auto& onfinally : finallyHandlers) { + onfinally(localError, localResult); + } + }); + return self(); + } + + Promise reject(QString error) { + return reject(error, QVariantMap()); + } + Promise reject(QString error, const QVariantMap& result) { + setState(false, error, result); + + executeOnPromiseThread([&]{ + const QString localError{ getError() }; + const QVariantMap localResult{ getResult() }; + HandlerFunctions rejectHandlers; + HandlerFunctions finallyHandlers; + withReadLock([&]{ + rejectHandlers = _onreject; + finallyHandlers = _onfinally; + }); + for (const auto& onreject : rejectHandlers) { + onreject(localError, localResult); + } + for (const auto& onfinally : finallyHandlers) { + onfinally(localError, localResult); + } + }); + return self(); + } + +private: + + Promise setState(bool resolved, QString error, const QVariantMap& result) { + if (resolved) { + _resolved = true; + } else { + _rejected = true; + } + setError(error); + assignResult(result); + return self(); + } + + void setError(const QString error) { withWriteLock([&]{ _error = error; }); } + QString getError() const { return resultWithReadLock([this]() -> QString { return _error; }); } + QVariantMap getResult() const { return resultWithReadLock([this]() -> QVariantMap { return _result; }); } + int getPendingHandlerCount() const { + return resultWithReadLock([this]() -> int { + return _onresolve.size() + _onreject.size() + _onfinally.size(); + }); + } + QString getStateString() const { + return _rejected ? "rejected" : + _resolved ? "resolved" : + getPendingHandlerCount() ? "pending" : + "unknown"; + } + QString _error; + QVariantMap _result; + std::atomic _rejected{false}; + std::atomic _resolved{false}; + HandlerFunctions _onresolve; + HandlerFunctions _onreject; + HandlerFunctions _onfinally; +}; + +Q_DECLARE_METATYPE(MiniPromise::Promise) + +inline MiniPromise::Promise makePromise(const QString& hint = QString()) { + if (!QMetaType::isRegistered(qMetaTypeId())) { + int type = qRegisterMetaType(); + qDebug() << "makePromise -- registered MetaType:" << type; + } + return std::make_shared(hint); +} diff --git a/script-archive/avatarSelector.js b/script-archive/avatarSelector.js index 47740ef0b3..119044e35a 100644 --- a/script-archive/avatarSelector.js +++ b/script-archive/avatarSelector.js @@ -274,7 +274,7 @@ function actionStartEvent(event) { var result = Overlays.findRayIntersection(event.actionRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { @@ -338,7 +338,7 @@ function handleLookAt(pickRay) { // check if we hit a panel and if we should jump there var result = Overlays.findRayIntersection(pickRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { var panelIndex = parseInt(panelName.slice(5)); diff --git a/script-archive/dressing_room/doppelganger.js b/script-archive/dressing_room/doppelganger.js index 10f6468e9f..773a5d974b 100644 --- a/script-archive/dressing_room/doppelganger.js +++ b/script-archive/dressing_room/doppelganger.js @@ -15,7 +15,7 @@ var TEST_MODEL_URL = 'https://s3.amazonaws.com/hifi-public/ozan/avatars/albert/a var MIRROR_JOINT_DATA = true; var MIRRORED_ENTITY_SCRIPT_URL = Script.resolvePath('mirroredEntity.js'); -var FREEZE_TOGGLER_SCRIPT_URL = Script.resolvePath('freezeToggler.js?' + Math.random(0, 1000)) +var FREEZE_TOGGLER_SCRIPT_URL = Script.resolvePath('freezeToggler.js?' + Math.random(0, 1000)); var THROTTLE = false; var THROTTLE_RATE = 100; var doppelgangers = []; @@ -24,8 +24,7 @@ function Doppelganger(avatar) { this.initialProperties = { name: 'Hifi-Doppelganger', type: 'Model', - modelURL: TEST_MODEL_URL, - // dimensions: getAvatarDimensions(avatar), + modelURL: MyAvatar.skeletonModelURL, position: putDoppelgangerAcrossFromAvatar(this, avatar), rotation: rotateDoppelgangerTowardAvatar(this, avatar), collisionsWillMove: false, @@ -48,7 +47,7 @@ function getJointData(avatar) { var jointNames = MyAvatar.jointNames; jointNames.forEach(function(joint, index) { var translation = MyAvatar.getJointTranslation(index); - var rotation = MyAvatar.getJointRotation(index) + var rotation = MyAvatar.getJointRotation(index); allJointData.push({ joint: joint, index: index, @@ -66,8 +65,8 @@ function setJointData(doppelganger, relativeXforms) { for (i = 0; i < l; i++) { jointRotations.push(relativeXforms[i].rot); } - Entities.setAbsoluteJointRotationsInObjectFrame(doppelganger.id, jointRotations); + Entities.setLocalJointRotations(doppelganger.id, jointRotations); return true; } @@ -127,7 +126,7 @@ var JOINT_MIRROR_NAME_MAP = { LeftHandPinky2: "RightHandPinky2", LeftHandPinky3: "RightHandPinky3", LeftHandPinky4: "RightHandPinky4", - LeftHandPinky: "RightHandPinky", + LeftHandPinky: "RightHandPinky" }; // maps joint names to parent joint names. @@ -192,7 +191,7 @@ var JOINT_PARENT_NAME_MAP = { LeftHandPinky1: "LeftHand", LeftHandPinky2: "LeftHandPinky1", LeftHandPinky3: "LeftHandPinky2", - LeftHandPinky: "LeftHandPinky3", + LeftHandPinky: "LeftHandPinky3" }; // maps joint indices to parent joint indices. @@ -206,7 +205,7 @@ function Xform(rot, pos) { }; Xform.ident = function () { return new Xform({x: 0, y: 0, z: 0, w: 1}, {x: 0, y: 0, z: 0}); -} +}; Xform.mul = function (lhs, rhs) { var rot = Quat.multiply(lhs.rot, rhs.rot); var pos = Vec3.sum(lhs.pos, Vec3.multiplyQbyV(lhs.rot, rhs.pos)); @@ -354,12 +353,12 @@ function getAvatarFootOffset() { if (jointName === "RightToe_End") { toeTop = d.translation.y } - }) + }); var myPosition = MyAvatar.position; var offset = upperLeg + lowerLeg + foot + toe + toeTop; offset = offset / 100; - return offset + return offset; } @@ -395,7 +394,7 @@ function connectDoppelgangerUpdates() { } function disconnectDoppelgangerUpdates() { - print('SHOULD DISCONNECT') + print('SHOULD DISCONNECT'); if (isConnected === true) { Script.update.disconnect(updateDoppelganger); } @@ -465,13 +464,13 @@ function handleFreezeMessages(channel, message, sender) { } catch (e) { print('error parsing wearable message'); } - print('MESSAGE ACTION::' + parsedMessage.action) + print('MESSAGE ACTION::' + parsedMessage.action); if (parsedMessage.action === 'freeze') { - print('ACTUAL FREEZE') + print('ACTUAL FREEZE'); disconnectDoppelgangerUpdates(); } if (parsedMessage.action === 'unfreeze') { - print('ACTUAL UNFREEZE') + print('ACTUAL UNFREEZE'); connectDoppelgangerUpdates(); } @@ -496,7 +495,7 @@ function handleWearableMessages(channel, message, sender) { } catch (e) { print('error parsing wearable message'); } - print('parsed message!!!') + print('parsed message!!!'); if (channel === 'Hifi-Doppelganger-Wearable') { mirrorEntitiesForDoppelganger(doppelgangers[0], parsedMessage); @@ -511,13 +510,13 @@ function mirrorEntitiesForDoppelganger(doppelganger, parsedMessage) { var doppelgangerProps = Entities.getEntityProperties(doppelganger.id); var action = parsedMessage.action; - print('IN MIRROR ENTITIES CALL' + action) + print('IN MIRROR ENTITIES CALL' + action); var baseEntity = parsedMessage.baseEntity; var wearableProps = Entities.getEntityProperties(baseEntity); - print('WEARABLE PROPS::') + print('WEARABLE PROPS::'); delete wearableProps.id; delete wearableProps.created; delete wearableProps.age; @@ -545,15 +544,15 @@ function mirrorEntitiesForDoppelganger(doppelganger, parsedMessage) { baseEntity: baseEntity, doppelganger: doppelganger.id } - }) + }); var mirrorEntity = Entities.addEntity(wearableProps); - var mirrorEntityProps = Entities.getEntityProperties(mirrorEntity) - print('MIRROR PROPS::' + JSON.stringify(mirrorEntityProps)) + var mirrorEntityProps = Entities.getEntityProperties(mirrorEntity); + print('MIRROR PROPS::' + JSON.stringify(mirrorEntityProps)); var wearablePair = { baseEntity: baseEntity, mirrorEntity: mirrorEntity - } + }; wearablePairs.push(wearablePair); } @@ -563,11 +562,11 @@ function mirrorEntitiesForDoppelganger(doppelganger, parsedMessage) { var mirrorEntity = getMirrorEntityForBaseEntity(baseEntity); // print('MIRROR ENTITY, newPosition' + mirrorEntity + ":::" + JSON.stringify(newPosition)) - Entities.editEntity(mirrorEntity, wearableProps) + Entities.editEntity(mirrorEntity, wearableProps); } if (action === 'remove') { - Entities.deleteEntity(getMirrorEntityForBaseEntity(baseEntity)) + Entities.deleteEntity(getMirrorEntityForBaseEntity(baseEntity)); wearablePairs = wearablePairs.filter(function(obj) { return obj.baseEntity !== baseEntity; }); @@ -575,7 +574,7 @@ function mirrorEntitiesForDoppelganger(doppelganger, parsedMessage) { if (action === 'updateBase') { //this gets called when the mirrored entity gets grabbed. now we move the - var mirrorEntityProperties = Entities.getEntityProperties(message.mirrorEntity) + var mirrorEntityProperties = Entities.getEntityProperties(message.mirrorEntity); var doppelgangerToMirrorEntity = Vec3.subtract(doppelgangerProps.position, mirrorEntityProperties.position); var newPosition = Vec3.sum(MyAvatar.position, doppelgangerToMirrorEntity); @@ -596,7 +595,7 @@ function getMirrorEntityForBaseEntity(baseEntity) { if (result.length === 0) { return false; } else { - return result[0].mirrorEntity + return result[0].mirrorEntity; } } @@ -607,13 +606,12 @@ function getBaseEntityForMirrorEntity(mirrorEntity) { if (result.length === 0) { return false; } else { - return result[0].baseEntity + return result[0].baseEntity; } } makeDoppelgangerForMyAvatar(); subscribeToWearableMessages(); -subscribeToWearableMessagesForAvatar(); subscribeToFreezeMessages(); function cleanup() { @@ -622,8 +620,8 @@ function cleanup() { } doppelgangers.forEach(function(doppelganger) { - print('DOPPELGANGER' + doppelganger.id) + print('DOPPELGANGER' + doppelganger.id); Entities.deleteEntity(doppelganger.id); }); } -Script.scriptEnding.connect(cleanup); \ No newline at end of file +Script.scriptEnding.connect(cleanup); diff --git a/script-archive/lobby.js b/script-archive/lobby.js index 6fa4a42cb6..7a06cdd906 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -272,7 +272,7 @@ function actionStartEvent(event) { var result = Overlays.findRayIntersection(event.actionRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { @@ -321,7 +321,7 @@ function handleLookAt(pickRay) { // check if we hit a panel and if we should jump there var result = Overlays.findRayIntersection(pickRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { var panelIndex = parseInt(panelName.slice(5)); diff --git a/scripts/developer/tests/unit_tests/assetUnitTests.js b/scripts/developer/tests/unit_tests/assetUnitTests.js new file mode 100644 index 0000000000..5d43eaf1de --- /dev/null +++ b/scripts/developer/tests/unit_tests/assetUnitTests.js @@ -0,0 +1,502 @@ +/* eslint-env jasmine */ + +if (!Entities.canWriteAssets()) { + Window.alert('!Entities.canWriteAssets -- please goto a domain with asset rights and reload this test script'); + throw new Error('!Entities.canWriteAssets'); +} + +instrument_testrunner(true); + +describe("Assets", function () { + var context = { + memo: {}, + cache: {}, + definedHash: null, + definedPath: null, + definedContent: null, + }; + var ATP_TIMEOUT_MS = 1000; // ms + + var NULL_HASH = new Array(64+1).join('0'); // 64 hex + var SAMPLE_FOLDER = '/assetUnitTests'; + var SAMPLE_FILE_PATH = SAMPLE_FOLDER + "/test.json"; + var SAMPLE_CONTENTS = 'Test Run on ' + JSON.stringify(Date()); + var SAMPLE_JSON = JSON.stringify({ content: SAMPLE_CONTENTS }); + var SAMPLE_FLOATS = [ Math.PI, 1.1, 2.2, 3.3 ]; + var SAMPLE_BUFFER = new Float32Array(SAMPLE_FLOATS).buffer; + + var IS_ASSET_HASH_REGEX = /^[a-f0-9]{64}$/i; + var IS_ASSET_URL = /^atp:/; + it('Entities.canWriteAssets', function() { + expect(Entities.canWriteAssets()).toBe(true); + }); + + describe('extractAssetHash(input)', function() { + // new helper method that provides a catch-all way to get at the sha256 hash + // considering the multiple, different ways ATP hash URLs are found across the + // system and in content. + + var POSITIVE_TEST_URLS = [ + 'atp:HASH', + 'atp:HASH.obj', + 'atp:HASH.fbx#cache-buster', + 'atp:HASH.fbx?cache-buster', + 'HASH' + ]; + var NEGATIVE_TEST_URLS = [ + 'asdf', + 'http://domain.tld', + '/path/filename.fbx', + 'atp:/path/filename.fbx?#HASH', + 'atp:/.baked/HASH/asset.fbx', + '' + ]; + it("POSITIVE_TEST_URLS", function() { + POSITIVE_TEST_URLS.forEach(function(url) { + url = url.replace('HASH', NULL_HASH); + expect([url, Assets.extractAssetHash(url)].join('|')).toEqual([url, NULL_HASH].join('|')); + }); + }); + it("NEGATIVE_TEST_URLS", function() { + NEGATIVE_TEST_URLS.forEach(function(url) { + expect(Assets.extractAssetHash(url.replace('HASH', NULL_HASH))).toEqual(''); + }); + }); + }); + + // new APIs + describe('.putAsset', function() { + it("data", function(done) { + Assets.putAsset(SAMPLE_CONTENTS, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.stringURL = result.url; + done(); + }); + }); + it('nopath.text', function(done) { + Assets.putAsset({ + data: SAMPLE_CONTENTS, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.textHash = result.hash; + context.memo.textURL = result.url; + done(); + }); + }); + it('path.text', function(done) { + var samplePath = SAMPLE_FOLDER + '/content.json'; + Assets.putAsset({ + path: samplePath, + data: SAMPLE_JSON, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.path).toEqual(samplePath); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.jsonPath = result.path; + done(); + }); + }); + it('path.buffer', function(done) { + var samplePath = SAMPLE_FOLDER + '/content.buffer'; + Assets.putAsset({ + path: samplePath, + data: SAMPLE_BUFFER, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.path).toEqual(samplePath); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + expect(result.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + context.memo.bufferURL = result.url; + context.memo.bufferHash = result.hash; + done(); + }); + }); + it('path.json.gz', function(done) { + var samplePath = SAMPLE_FOLDER + '/content.json.gz'; + Assets.putAsset({ + path: samplePath, + data: SAMPLE_JSON, + compress: true, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.path).toEqual(samplePath); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.jsonCompressedPath = result.path; + done(); + }); + }); + }); + // it('.deleteAsset(options, {callback(error, result)})', function() { pending(); }); + it('.resolveAsset', function(done) { + expect(context.memo.bufferURL).toBeDefined(); + Assets.resolveAsset(context.memo.bufferURL, function(error, result) { + expect(error).toBe(null); + expect(result.url).toEqual(context.memo.bufferURL); + expect(result.hash).toEqual(context.memo.bufferHash); + done(); + }); + }); + describe('.getAsset', function() { + it('path/', function(done) { + expect(context.memo.jsonPath).toBeDefined(); + Assets.getAsset(context.memo.jsonPath, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_JSON); + done(); + }); + }); + it('text', function(done) { + expect(context.memo.textURL).toBeDefined(); + Assets.getAsset({ + url: context.memo.textURL, + responseType: 'text', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_CONTENTS); + expect(result.url).toEqual(context.memo.textURL); + done(); + }); + }); + it('arraybuffer', function(done) { + expect(context.memo.bufferURL).toBeDefined(); + Assets.getAsset({ + url: context.memo.bufferURL, + responseType: 'arraybuffer', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + var expected = [].slice.call(new Float32Array(SAMPLE_BUFFER)).join(','); + var actual = [].slice.call(new Float32Array(result.response)).join(','); + expect(actual).toEqual(expected); + done(); + }); + }); + it('json', function(done) { + expect(context.memo.jsonPath).toBeDefined(); + Assets.getAsset({ + url: context.memo.jsonPath, + responseType: 'json', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.content).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + it('json.gz', function(done) { + expect(context.memo.jsonCompressedPath).toBeDefined(); + Assets.getAsset({ + url: context.memo.jsonCompressedPath, + responseType: 'json', + decompress: true, + }, function(error, result) { + expect(error).toBe(null); + expect(result.decompressed).toBe(true); + expect(result.response.content).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + }); + + // cache APIs + it('.getCacheStatus', function(done) { + Assets.getCacheStatus(function(error, result) { + expect(error).toBe(null); + expect(result.cacheSize).toBeGreaterThan(0); + expect(result.maximumCacheSize).toBeGreaterThan(0); + done(); + }); + }); + describe('.saveToCache', function() { + it(".data", function(done) { + Assets.saveToCache({ data: SAMPLE_CONTENTS }, function(error, result) { + expect(error).toBe(null); + expect(result.success).toBe(true); + context.cache.textURL = result.url; + done(); + }); + }); + it('.url', function(done) { + var sampleURL = 'atp:' + SAMPLE_FOLDER + '/cache.json'; + Assets.saveToCache({ + url: sampleURL, + data: SAMPLE_JSON, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.url).toEqual(sampleURL); + expect(result.success).toBe(true); + context.cache.jsonURL = result.url; + done(); + }); + }); + it('.buffer', function(done) { + var sampleURL = 'atp:' + SAMPLE_FOLDER + '/cache.buffer'; + Assets.saveToCache({ + url: sampleURL, + data: SAMPLE_BUFFER, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.url).toEqual(sampleURL); + expect(result.success).toBe(true); + expect(result.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + context.cache.bufferURL = result.url; + done(); + }); + }); + }); + it('.queryCacheMeta', function(done) { + expect(context.cache.bufferURL).toBeDefined(); + Assets.queryCacheMeta(context.cache.bufferURL, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.url).toEqual(context.cache.bufferURL); + done(); + }); + }); + describe('.loadFromCache', function() { + it('urlString', function(done) { + expect(context.cache.jsonURL).toBeDefined(); + Assets.loadFromCache(context.cache.jsonURL, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_JSON); + done(); + }); + }); + it('.responseType=text', function(done) { + expect(context.cache.textURL).toBeDefined(); + Assets.loadFromCache({ + url: context.cache.textURL, + responseType: 'text', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + it('.responseType=arraybuffer', function(done) { + expect(context.cache.bufferURL).toBeDefined(); + Assets.loadFromCache({ + url: context.cache.bufferURL, + responseType: 'arraybuffer', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + var expected = [].slice.call(new Float32Array(SAMPLE_BUFFER)).join(','); + var actual = [].slice.call(new Float32Array(result.response)).join(','); + expect(actual).toEqual(expected); + done(); + }); + }); + it('.responseType=json', function(done) { + expect(context.cache.jsonURL).toBeDefined(); + Assets.loadFromCache({ + url: context.cache.jsonURL, + responseType: 'json', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.content).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + }); + + // existing newly-mapped APIs + it('.getATPUrl', function() { + expect(Assets.getATPUrl(SAMPLE_FILE_PATH)).toEqual('atp:' + SAMPLE_FILE_PATH); + expect(Assets.getATPUrl(NULL_HASH)).toEqual('atp:' + NULL_HASH); + expect(Assets.getATPUrl("/file.ext?a=b#c=d")).toEqual('atp:/file.ext?a=b#c=d'); + expect(Assets.getATPUrl("atp:xxx")).toEqual(''); + expect(Assets.getATPUrl("xxx")).toEqual(''); + }); + xit('.isValidPath', function(done) { pending(); }); + xit('.isValidFilePath', function() { pending(); }); + xit('.isValidHash', function() { pending(); }); + xit('.setBakingEnabled', function() { pending(); }); + + it("Assets.uploadData(data, {callback(url, hash)})", function (done) { + Assets.uploadData(SAMPLE_CONTENTS, function(url, hash) { + expect(url).toMatch(IS_ASSET_URL); + expect(hash).toMatch(IS_ASSET_HASH_REGEX); + context.definedHash = hash; // used in later tests + context.definedContent = SAMPLE_CONTENTS; + print('context.definedHash = ' + context.definedHash); + print('context.definedContent = ' + context.definedContent); + done(); + }); + }); + + it("Assets.setMapping", function (done) { + expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX); + Assets.setMapping(SAMPLE_FILE_PATH, context.definedHash, function(error) { + if (error) error += ' ('+JSON.stringify([SAMPLE_FILE_PATH, context.definedHash])+')'; + expect(error).toBe(null); + context.definedPath = SAMPLE_FILE_PATH; + print('context.definedPath = ' + context.definedPath); + done(); + }); + }); + + it("Assets.getMapping", function (done) { + expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX, 'asfasdf'); + expect(context.definedPath).toMatch(/^\//, 'asfasdf'); + Assets.getMapping(context.definedPath, function(error, hash) { + if (error) error += ' ('+JSON.stringify([context.definedPath])+')'; + expect(error).toBe(null); + expect(hash).toMatch(IS_ASSET_HASH_REGEX); + expect(hash).toEqual(context.definedHash); + done(); + }); + }); + + it('.getMapping(nullHash)', function(done) { + // FIXME: characterization test -- current behavior is that downloadData silently fails + // when given an asset that doesn't exist + Assets.downloadData(NULL_HASH, function(result) { + throw new Error("this callback 'should' not be triggered"); + }); + setTimeout(function() { done(); }, ATP_TIMEOUT_MS); + }); + + it('.downloadData(hash)', function(done) { + expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX, 'asfasdf'); + Assets.downloadData('atp:' + context.definedHash, function(result) { + expect(result).toEqual(context.definedContent); + done(); + }); + }); + + it('.downloadData(nullHash)', function(done) { + // FIXME: characterization test -- current behavior is that downloadData silently fails + // when given an asset doesn't exist + Assets.downloadData(NULL_HASH, function(result) { + throw new Error("this callback 'should' not be triggered"); + }); + setTimeout(function() { done(); }, ATP_TIMEOUT_MS); + }); + + describe('exceptions', function() { + describe('.getAsset', function() { + it('-- invalid data.gz', function(done) { + expect(context.memo.jsonPath).toBeDefined(); + Assets.getAsset({ + url: context.memo.jsonPath, + responseType: 'json', + decompress: true, + }, function(error, result) { + expect(error).toMatch(/gunzip error/); + done(); + }); + }); + }); + describe('.setMapping', function() { + it('-- invalid path', function(done) { + Assets.setMapping('foo', NULL_HASH, function(error/*, result*/) { + expect(error).toEqual('Path is invalid'); + /* expect(result).not.toMatch(IS_ASSET_URL); */ + done(); + }); + }); + it('-- invalid hash', function(done) { + Assets.setMapping(SAMPLE_FILE_PATH, 'bar', function(error/*, result*/) { + expect(error).toEqual('Hash is invalid'); + /* expect(result).not.toMatch(IS_ASSET_URL); */ + done(); + }); + }); + }); + describe('.getMapping', function() { + it('-- invalid path throws immediate', function() { + expect(function() { + Assets.getMapping('foo', function(error, hash) { + throw new Error("should never make it here..."); + }); + throw new Error("should never make it here..."); + }).toThrowError(/invalid.*path/i); + }); + it('-- non-existing path', function(done) { + Assets.getMapping('/foo/bar/'+Date.now(), function(error, hash) { + expect(error).toEqual('Asset not found'); + expect(hash).not.toMatch(IS_ASSET_HASH_REGEX); + done(); + }); + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// this stuff allows the unit tests to be loaded indepenently and/or as part of testRunner.js execution +function run() {} +function instrument_testrunner(force) { + if (force || typeof describe === 'undefined') { + var oldPrint = print; + window = new OverlayWebWindow({ + title: 'assetUnitTests.js', + width: 640, + height: 480, + source: 'about:blank', + }); + Script.scriptEnding.connect(window, 'close'); + window.closed.connect(Script, 'stop'); + // wait for window (ready) and test runner (ran) both to initialize + var ready = false; + var ran = false; + window.webEventReceived.connect(function() { ready = true; maybeRun(); }); + run = function() { ran = true; maybeRun(); }; + + window.setURL([ + 'data:text/html;text,', + '', + '
', + '', + '', + ].join('\n')); + print = function() { + var str = [].slice.call(arguments).join(' '); + if (ready) { + window.emitScriptEvent(str + '\n'); + } else { + oldPrint('!ready', str); + } + }; + + // Include testing library + Script.include('../../libraries/jasmine/jasmine.js'); + Script.include('../../libraries/jasmine/hifi-boot.js'); + + function maybeRun() { + if (!ran || !ready) { + return oldPrint('doRun -- waiting for both script and web window to become available'); + } + if (!force) { + // invoke Script.stop (after any async tests complete) + jasmine.getEnv().addReporter({ jasmineDone: Script.stop }); + } else { + jasmine.getEnv().addReporter({ jasmineDone: function() { print("JASMINE DONE"); } }); + } + + // Run the tests + jasmine.getEnv().execute(); + }; + } +} +run(); diff --git a/tests/gl/CMakeLists.txt b/tests/gl/CMakeLists.txt index 2b2b79d8b2..40bb64be1c 100644 --- a/tests/gl/CMakeLists.txt +++ b/tests/gl/CMakeLists.txt @@ -1,6 +1,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() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(shared gl) package_libraries_for_deployment() diff --git a/tests/shared/src/BitVectorHelperTests.cpp b/tests/shared/src/BitVectorHelperTests.cpp new file mode 100644 index 0000000000..070e90eec7 --- /dev/null +++ b/tests/shared/src/BitVectorHelperTests.cpp @@ -0,0 +1,80 @@ +// +// BitVectorHelperTests.cpp +// tests/shared/src +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "BitVectorHelperTests.h" + +#include + +#include "../QTestExtensions.h" +#include +#include +#include +#include + +QTEST_MAIN(BitVectorHelperTests) + +const int BITS_IN_BYTE = 8; + +void BitVectorHelperTests::sizeTest() { + std::vector sizes = {0, 6, 7, 8, 30, 31, 32, 33, 87, 88, 89, 90, 90, 91, 92, 93}; + for (auto& size : sizes) { + const int oldWay = (int)ceil((float)size / (float)BITS_IN_BYTE); + const int newWay = (int)calcBitVectorSize(size); + QCOMPARE(oldWay, newWay); + } +} + +static void readWriteHelper(const std::vector& src) { + + int numBits = (int)src.size(); + int numBytes = calcBitVectorSize(numBits); + uint8_t* bytes = new uint8_t[numBytes]; + memset(bytes, numBytes, sizeof(uint8_t)); + int numBytesWritten = writeBitVector(bytes, numBits, [&](int i) { + return src[i]; + }); + QCOMPARE(numBytesWritten, numBytes); + + std::vector dst; + int numBytesRead = readBitVector(bytes, numBits, [&](int i, bool value) { + dst.push_back(value); + }); + QCOMPARE(numBytesRead, numBytes); + + QCOMPARE(numBits, (int)src.size()); + QCOMPARE(numBits, (int)dst.size()); + for (int i = 0; i < numBits; i++) { + bool a = src[i]; + bool b = dst[i]; + QCOMPARE(a, b); + } +} + +void BitVectorHelperTests::readWriteTest() { + std::vector sizes = {0, 6, 7, 8, 30, 31, 32, 33, 87, 88, 89, 90, 90, 91, 92, 93}; + + for (auto& size : sizes) { + std::vector allTrue(size, true); + std::vector allFalse(size, false); + std::vector evenSet; + evenSet.reserve(size); + std::vector oddSet; + oddSet.reserve(size); + for (int i = 0; i < size; i++) { + bool isOdd = (i & 0x1) > 0; + evenSet.push_back(!isOdd); + oddSet.push_back(isOdd); + } + readWriteHelper(allTrue); + readWriteHelper(allFalse); + readWriteHelper(evenSet); + readWriteHelper(oddSet); + } +} diff --git a/tests/shared/src/BitVectorHelperTests.h b/tests/shared/src/BitVectorHelperTests.h new file mode 100644 index 0000000000..1f52ba1ac9 --- /dev/null +++ b/tests/shared/src/BitVectorHelperTests.h @@ -0,0 +1,23 @@ +// +// BitVectorHelperTests.h +// tests/shared/src +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BitVectorHelperTests_h +#define hifi_BitVectorHelperTests_h + +#include + +class BitVectorHelperTests : public QObject { + Q_OBJECT +private slots: + void sizeTest(); + void readWriteTest(); +}; + +#endif // hifi_BitVectorHelperTests_h diff --git a/tools/atp-client/src/ATPClientApp.cpp b/tools/atp-client/src/ATPClientApp.cpp index 9fd1bf8d4f..307c913a96 100644 --- a/tools/atp-client/src/ATPClientApp.cpp +++ b/tools/atp-client/src/ATPClientApp.cpp @@ -361,7 +361,7 @@ void ATPClientApp::lookupAsset() { request->start(); } -void ATPClientApp::download(AssetHash hash) { +void ATPClientApp::download(AssetUtils::AssetHash hash) { auto assetClient = DependencyManager::get(); auto assetRequest = new AssetRequest(hash); diff --git a/tools/atp-client/src/ATPClientApp.h b/tools/atp-client/src/ATPClientApp.h index a3904d6e50..42760ceca9 100644 --- a/tools/atp-client/src/ATPClientApp.h +++ b/tools/atp-client/src/ATPClientApp.h @@ -45,7 +45,7 @@ private: void setMapping(QString hash); void lookupAsset(); void listAssets(); - void download(AssetHash hash); + void download(AssetUtils::AssetHash hash); void finish(int exitCode); bool _verbose; diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp index 6c637ab404..816eac7fbd 100644 --- a/tools/auto-tester/src/Test.cpp +++ b/tools/auto-tester/src/Test.cpp @@ -306,8 +306,8 @@ void Test::evaluateTestsRecursively(bool interactiveMode, QProgressBar* progress zipAndDeleteTestResultsFolder(); } -void Test::importTest(QTextStream& textStream, const QString& testPathname, int testNumber) { - textStream << "var test" << testNumber << " = Script.require(\"" << "file:///" << testPathname + "\");" << endl; +void Test::importTest(QTextStream& textStream, const QString& testPathname) { + textStream << "Script.include(\"" << "file:///" << testPathname + "?raw=true\");" << endl; } // Creates a single script in a user-selected folder. @@ -330,12 +330,14 @@ void Test::createRecursiveScript() { } QTextStream textStream(&allTestsFilename); - textStream << "// This is an automatically generated file, created by auto-tester" << endl; + textStream << "// This is an automatically generated file, created by auto-tester" << endl << endl; + + textStream << "var autoTester = Script.require(\"https://github.com/highfidelity/hifi_tests/blob/master/tests/utils/autoTester.js?raw=true\");" << endl; + textStream << "autoTester.enableRecursive();" << endl << endl; // The main will call each test after the previous test is completed // This is implemented with an interval timer that periodically tests if a // running test has increment a testNumber variable that it received as an input. - int testNumber = 1; QVector testPathnames; // First test if top-level folder has a test.js file @@ -343,8 +345,7 @@ void Test::createRecursiveScript() { QFileInfo fileInfo(testPathname); if (fileInfo.exists()) { // Current folder contains a test - importTest(textStream, testPathname, testNumber); - ++testNumber; + importTest(textStream, testPathname); testPathnames << testPathname; } @@ -363,8 +364,7 @@ void Test::createRecursiveScript() { QFileInfo fileInfo(testPathname); if (fileInfo.exists()) { // Current folder contains a test - importTest(textStream, testPathname, testNumber); - ++testNumber; + importTest(textStream, testPathname); testPathnames << testPathname; } @@ -377,79 +377,9 @@ void Test::createRecursiveScript() { } textStream << endl; - - // Define flags for each test - for (int i = 1; i <= testPathnames.length(); ++i) { - textStream << "var test" << i << "HasNotStarted = true;" << endl; - } - - // Leave a blank line in the main - textStream << endl; - - const int TEST_PERIOD = 1000; // in milliseconds - const QString tab = " "; - - textStream << "// Check every second if the current test is complete and the next test can be run" << endl; - textStream << "var testTimer = Script.setInterval(" << endl; - textStream << tab << "function() {" << endl; - - const QString testFunction = "test"; - for (int i = 1; i <= testPathnames.length(); ++i) { - // First test starts immediately, all other tests wait for the previous test to complete. - // The script produced will look as follows: - // if (test1HasNotStarted) { - // test1HasNotStarted = false; - // test1.test("auto"); - // print("******started test 1******"); - // } - // | - // | - // if (test5.complete && test6HasNotStarted) { - // test6HasNotStarted = false; - // test7.test(); - // print("******started test 6******"); - // } - // | - // | - // if (test12.complete) { - // print("******stopping******"); - // Script.stop(); - // } - // - if (i == 1) { - textStream << tab << tab << "if (test1HasNotStarted) {" << endl; - } else { - textStream << tab << tab << "if (test" << i - 1 << ".complete && test" << i << "HasNotStarted) {" << endl; - } - textStream << tab << tab << tab << "test" << i << "HasNotStarted = false;" << endl; - textStream << tab << tab << tab << "test" << i << "." << testFunction << "(\"auto\");" << endl; - textStream << tab << tab << tab << "print(\"******started test " << i << "******\");" << endl; - - textStream << tab << tab << "}" << endl << endl; - - } - - // Add extra step to stop the script - textStream << tab << tab << "if (test" << testPathnames.length() << ".complete) {" << endl; - textStream << tab << tab << tab << "print(\"******stopping******\");" << endl; - textStream << tab << tab << tab << "Script.stop();" << endl; - textStream << tab << tab << "}" << endl << endl; - - textStream << tab << "}," << endl; - textStream << endl; - textStream << tab << TEST_PERIOD << endl; - textStream << ");" << endl << endl; - - textStream << "// Stop the timer and clear the module cache" << endl; - textStream << "Script.scriptEnding.connect(" << endl; - textStream << tab << "function() {" << endl; - textStream << tab << tab << "Script.clearInterval(testTimer);" << endl; - textStream << tab << tab << "Script.require.cache = {};" << endl; - textStream << tab << "}" << endl; - textStream << ");" << endl; + textStream << "autoTester.runRecursive();" << endl; allTestsFilename.close(); - messageBox.information(0, "Success", "Script has been created"); } diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h index aa1346fa2a..3177df4d47 100644 --- a/tools/auto-tester/src/Test.h +++ b/tools/auto-tester/src/Test.h @@ -36,7 +36,7 @@ public: bool isInSnapshotFilenameFormat(QString filename); bool isInExpectedImageFilenameFormat(QString filename); - void importTest(QTextStream& textStream, const QString& testPathname, int testNumber); + void importTest(QTextStream& textStream, const QString& testPathname); void appendTestResultsToFile(QString testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage);