/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include "BuilderSettingManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ImageProcessingAtom { const char* BuilderSettingManager::s_defaultConfigRelativeFolder = "Gems/Atom/Asset/ImageProcessingAtom/Assets/Config/"; const char* BuilderSettingManager::s_projectConfigRelativeFolder = "Config/AtomImageBuilder/"; const char* BuilderSettingManager::s_builderSettingFileName = "ImageBuilder.settings"; const char* BuilderSettingManager::s_presetFileExtension = "preset"; const char FileMaskDelimiter = '_'; namespace { static constexpr const char* const LogWindow = "Image Processing"; } #if defined(AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS) #define AZ_RESTRICTED_PLATFORM_EXPANSION(CodeName, CODENAME, codename, PrivateName, PRIVATENAME, privatename, PublicName, PUBLICNAME, publicname, PublicAuxName1, PublicAuxName2, PublicAuxName3) \ namespace ImageProcess##PrivateName \ { \ bool DoesSupport(AZStd::string); \ } AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS #undef AZ_RESTRICTED_PLATFORM_EXPANSION #endif //AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS const char* BuilderSettingManager::s_environmentVariableName = "ImageBuilderSettingManager_Atom"; AZ::EnvironmentVariable BuilderSettingManager::s_globalInstance = nullptr; AZStd::mutex BuilderSettingManager::s_instanceMutex; const PlatformName BuilderSettingManager::s_defaultPlatform = AZ_TRAIT_IMAGEPROCESSING_DEFAULT_PLATFORM; void BuilderSettingManager::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serialize = azrtti_cast(context); if (serialize) { serialize->Class() ->Version(2) ->Field("BuildSettings", &BuilderSettingManager::m_builderSettings) ->Field("PresetsByFileMask", &BuilderSettingManager::m_presetFilterMap) ->Field("DefaultPreset", &BuilderSettingManager::m_defaultPreset) ->Field("DefaultPresetAlpha", &BuilderSettingManager::m_defaultPresetAlpha) ->Field("DefaultPresetNonePOT", &BuilderSettingManager::m_defaultPresetNonePOT) // deprecated properties ->Field("DefaultPresetsByFileMask", &BuilderSettingManager::m_defaultPresetByFileMask) ->Field("AnalysisFingerprint", &BuilderSettingManager::m_analysisFingerprint); } } BuilderSettingManager* BuilderSettingManager::Instance() { AZStd::lock_guard lock(s_instanceMutex); if (!s_globalInstance) { s_globalInstance = AZ::Environment::FindVariable(s_environmentVariableName); } AZ_Assert(s_globalInstance, "BuilderSettingManager not created!"); return s_globalInstance.Get(); } void BuilderSettingManager::CreateInstance() { AZStd::lock_guard lock(s_instanceMutex); if (s_globalInstance) { AZ_Assert(false, "BuilderSettingManager already created!"); return; } if (!s_globalInstance) { s_globalInstance = AZ::Environment::CreateVariable(s_environmentVariableName); } if (!s_globalInstance.Get()) { s_globalInstance.Set(aznew BuilderSettingManager()); } } void BuilderSettingManager::DestroyInstance() { AZStd::lock_guard lock(s_instanceMutex); AZ_Assert(s_globalInstance, "Invalid call to DestroyInstance - no instance exists."); AZ_Assert(s_globalInstance.Get(), "You can only call DestroyInstance if you have called CreateInstance."); delete s_globalInstance.Get(); s_globalInstance.Reset(); } const PresetSettings* BuilderSettingManager::GetPreset(const PresetName& presetName, const PlatformName& platform, AZStd::string_view* settingsFilePathOut) const { AZStd::lock_guard lock(m_presetMapLock); auto itr = m_presets.find(presetName); if (itr != m_presets.end()) { if (settingsFilePathOut) { *settingsFilePathOut = itr->second.m_presetFilePath; } return itr->second.m_multiPreset.GetPreset(platform); } return nullptr; } AZStd::vector BuilderSettingManager::GetFileMasksForPreset(const PresetName& presetName) const { AZStd::vector fileMasks; AZStd::lock_guard lock(m_presetMapLock); for (const auto& mapping:m_presetFilterMap) { for (const auto& preset : mapping.second) { if (preset == presetName) { fileMasks.push_back(mapping.first); break; } } } return fileMasks; } const BuilderSettings* BuilderSettingManager::GetBuilderSetting(const PlatformName& platform) const { auto itr = m_builderSettings.find(platform); if (itr != m_builderSettings.end()) { return &itr->second; } return nullptr; } const PlatformNameList BuilderSettingManager::GetPlatformList() const { PlatformNameList platforms; for (auto& builderSetting : m_builderSettings) { if (builderSetting.second.m_enablePlatform) { platforms.push_back(builderSetting.first); } } return platforms; } const AZStd::map >& BuilderSettingManager::GetPresetFilterMap() const { AZStd::lock_guard lock(m_presetMapLock); return m_presetFilterMap; } const AZStd::unordered_set& BuilderSettingManager::GetFullPresetList() const { AZStd::lock_guard lock(m_presetMapLock); AZStd::string noFilter = AZStd::string(); return m_presetFilterMap.find(noFilter)->second; } const PresetName BuilderSettingManager::GetPresetNameFromId(const AZ::Uuid& presetId) { AZStd::lock_guard lock(m_presetMapLock); for (const auto& itr : m_presets) { if (itr.second.m_multiPreset.GetPresetId() == presetId) { return itr.second.m_multiPreset.GetPresetName(); } } return {}; } void BuilderSettingManager::ClearSettings() { AZStd::lock_guard lock(m_presetMapLock); m_presetFilterMap.clear(); m_builderSettings.clear(); m_presets.clear(); } StringOutcome BuilderSettingManager::LoadConfig() { StringOutcome outcome = STRING_OUTCOME_ERROR(""); auto fileIoBase = AZ::IO::FileIOBase::GetInstance(); if (fileIoBase == nullptr) { return AZ::Failure( AZStd::string("File IO instance needs to be initialized to resolve ImageProcessing builder file aliases")); } if (auto engineRoot = fileIoBase->ResolvePath("@engroot@"); engineRoot.has_value()) { m_defaultConfigFolder = *engineRoot; m_defaultConfigFolder /= s_defaultConfigRelativeFolder; } if (auto sourceGameRoot = fileIoBase->ResolvePath("@projectroot@"); sourceGameRoot.has_value()) { m_projectConfigFolder = *sourceGameRoot; m_projectConfigFolder /= s_projectConfigRelativeFolder; } AZStd::lock_guard lock(m_presetMapLock); ClearSettings(); outcome = LoadSettings(); if (outcome.IsSuccess()) { // Load presets in default folder first, then load from project folder. // The same presets which loaded last will overwrite previous loaded one. LoadPresets(m_defaultConfigFolder.Native()); LoadPresets(m_projectConfigFolder.Native()); } // Collect extra file masks from preset files CollectFileMasksFromPresets(); if (QCoreApplication::instance()) { m_fileWatcher.reset(new QFileSystemWatcher); // track preset files // Note, the QT signal would only works for AP but not AssetBuilder // We use file time stamp to track preset file change in builder's CreateJob for (auto& preset : m_presets) { m_fileWatcher.data()->addPath(QString(preset.second.m_presetFilePath.c_str())); } m_fileWatcher.data()->addPath(QString(m_defaultConfigFolder.c_str())); m_fileWatcher.data()->addPath(QString(m_projectConfigFolder.c_str())); QObject::connect(m_fileWatcher.data(), &QFileSystemWatcher::fileChanged, this, &BuilderSettingManager::OnFileChanged); QObject::connect(m_fileWatcher.data(), &QFileSystemWatcher::directoryChanged, this, &BuilderSettingManager::OnFolderChanged); } return outcome; } void BuilderSettingManager::LoadPresets(AZStd::string_view presetFolder) { QDirIterator it(presetFolder.data(), QStringList() << "*.preset", QDir::Files, QDirIterator::NoIteratorFlags); while (it.hasNext()) { QString filePath = it.next(); LoadPreset(filePath.toUtf8().data()); } } bool BuilderSettingManager::LoadPreset(const AZStd::string& filePath) { QFileInfo fileInfo (filePath.c_str()); if (!fileInfo.exists()) { return false; } MultiplatformPresetSettings preset; auto result = AZ::JsonSerializationUtils::LoadObjectFromFile(preset, filePath); if (!result.IsSuccess()) { AZ_Warning(LogWindow, false, "Failed to load preset file %s. Error: %s", filePath.c_str(), result.GetError().c_str()); return false; } PresetName presetName(fileInfo.baseName().toUtf8().data()); AZ_Warning(LogWindow, presetName == preset.GetPresetName(), "Preset file name '%s' is not" " same as preset name '%s'. Using preset file name as preset name", filePath.c_str(), preset.GetPresetName().GetCStr()); preset.SetPresetName(presetName); m_presets[presetName] = PresetEntry{preset, filePath.c_str(), fileInfo.lastModified()}; return true; } void BuilderSettingManager::ReloadPreset(const PresetName& presetName) { // Find the preset file from project or default config folder AZStd::string presetFileName = AZStd::string::format("%s.%s", presetName.GetCStr(), s_presetFileExtension); AZ::IO::FixedMaxPath filePath = m_projectConfigFolder/presetFileName; QFileInfo fileInfo (filePath.c_str()); if (!fileInfo.exists()) { filePath = (m_defaultConfigFolder/presetFileName).c_str(); fileInfo = QFileInfo(filePath.c_str()); } AZStd::lock_guard lock(m_presetMapLock); //Skip the loading if the file wasn't chagned if (fileInfo.exists()) { if (m_presets.find(presetName) != m_presets.end()) { if (m_presets[presetName].m_lastModifiedTime == fileInfo.lastModified() && m_presets[presetName].m_presetFilePath == filePath.c_str()) { return; } } } // remove preset m_presets.erase(presetName); if (fileInfo.exists()) { LoadPreset(filePath.c_str()); } } StringOutcome BuilderSettingManager::LoadConfigFromFolder(AZStd::string_view configFolder) { AZStd::lock_guard lock(m_presetMapLock); // Load builder settings AZStd::string settingFilePath = AZStd::string::format("%.*s%s", aznumeric_cast(configFolder.size()), configFolder.data(), s_builderSettingFileName); auto result = LoadSettings(settingFilePath); // Load presets if (result.IsSuccess()) { LoadPresets(configFolder); } return result; } void BuilderSettingManager::ReportDeprecatedSettings() { // reported deprecated attributes in image builder settings if (!m_analysisFingerprint.empty()) { AZ_Warning(LogWindow, false, "'AnalysisFingerprint' is deprecated and it should be removed from file [%s]", s_builderSettingFileName); } if (!m_defaultPresetByFileMask.empty()) { AZ_Warning(LogWindow, false, "'DefaultPresetsByFileMask' is deprecated and it should be removed from file [%s]. Use PresetsByFileMask instead", s_builderSettingFileName); } } StringOutcome BuilderSettingManager::LoadSettings() { // If the project image build setting file exist, it will merge image builder settings from project folder to the settings from default config folder. bool needMerge = false; AZStd::string projectSettingFile{ (m_projectConfigFolder / s_builderSettingFileName).Native() }; if (AZ::IO::SystemFile::Exists(projectSettingFile.c_str())) { needMerge = true; } AZ::Outcome outcome; AZStd::string defaultSettingFile{ (m_defaultConfigFolder / s_builderSettingFileName).Native() }; if (needMerge) { auto outcome1 = AZ::JsonSerializationUtils::ReadJsonFile(defaultSettingFile); auto outcome2 = AZ::JsonSerializationUtils::ReadJsonFile(projectSettingFile); // return error if it failed to load default settings if (!outcome1.IsSuccess()) { return STRING_OUTCOME_ERROR(outcome1.GetError()); } // if project config was loaded successfully, apply merge patch rapidjson::Document& originDoc = outcome1.GetValue(); if (outcome2.IsSuccess()) { const rapidjson::Document& patchDoc = outcome2.GetValue(); AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::ApplyPatch(originDoc, originDoc.GetAllocator(), patchDoc, AZ::JsonMergeApproach::JsonMergePatch); if (result.GetProcessing() == AZ::JsonSerializationResult::Processing::Completed) { AZStd::vector outBuffer; AZ::IO::ByteContainerStream> outStream{ &outBuffer }; AZ::JsonSerializationUtils::WriteJsonStream(originDoc, outStream); outStream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN); outcome = AZ::JsonSerializationUtils::LoadObjectFromStream(*this, outStream); if (!outcome.IsSuccess()) { return STRING_OUTCOME_ERROR(outcome.GetError()); } ReportDeprecatedSettings(); // Generate config file fingerprint outStream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN); AZ::u64 hash = AssetBuilderSDK::GetHashFromIOStream(outStream); m_analysisFingerprint = AZStd::string::format("%llX", hash); } else { needMerge = false; AZ_Warning(LogWindow, false, "Failed to fully merge data into image builder settings. Skipping project build setting file [%s]", projectSettingFile.c_str()); } } else { AZ_Warning(LogWindow, false, "Failed to load project setting file [%s]. Skipping", projectSettingFile.c_str()); } } if (!needMerge) { outcome = AZ::JsonSerializationUtils::LoadObjectFromFile(*this, defaultSettingFile); if (!outcome.IsSuccess()) { return STRING_OUTCOME_ERROR(outcome.GetError()); } ReportDeprecatedSettings(); // Generate config file fingerprint AZ::u64 hash = AssetBuilderSDK::GetFileHash(defaultSettingFile.c_str()); m_analysisFingerprint = AZStd::string::format("%llX", hash); } return STRING_OUTCOME_SUCCESS; } StringOutcome BuilderSettingManager::LoadSettings(AZStd::string_view filepath) { AZStd::lock_guard lock(m_presetMapLock); auto result = AZ::JsonSerializationUtils::LoadObjectFromFile(*this, filepath); if (!result.IsSuccess()) { return STRING_OUTCOME_ERROR(result.GetError()); } //enable builder settings for enabled restricted platforms. These settings should be disabled by default in the setting file #if defined(AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS) #define AZ_RESTRICTED_PLATFORM_EXPANSION(CodeName, CODENAME, codename, PrivateName, PRIVATENAME, privatename, PublicName, PUBLICNAME, publicname, PublicAuxName1, PublicAuxName2, PublicAuxName3) \ for (auto& buildSetting : m_builderSettings) \ { \ if (ImageProcess##PrivateName::DoesSupport(buildSetting.first)) \ { \ buildSetting.second.m_enablePlatform = true; \ break; \ } \ } AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS #undef AZ_RESTRICTED_PLATFORM_EXPANSION #endif //AZ_TOOLS_EXPAND_FOR_RESTRICTED_PLATFORMS return STRING_OUTCOME_SUCCESS; } StringOutcome BuilderSettingManager::WriteSettings(AZStd::string_view filepath) { AZ::JsonSerializerSettings saveSettings; saveSettings.m_keepDefaults = true; auto result = AZ::JsonSerializationUtils::SaveObjectToFile(this, filepath, (BuilderSettingManager*)nullptr, &saveSettings); if (!result.IsSuccess()) { return STRING_OUTCOME_ERROR(result.GetError()); } return STRING_OUTCOME_SUCCESS; } const AZStd::string& BuilderSettingManager::GetAnalysisFingerprint() const { return m_analysisFingerprint; } void BuilderSettingManager::CollectFileMasksFromPresets() { AZStd::lock_guard lock(m_presetMapLock); AZStd::string noFilter = AZStd::string(); AZStd::string extraString; for (const auto& presetIter : m_presets) { const MultiplatformPresetSettings& multiPreset = presetIter.second.m_multiPreset; const PresetSettings& preset = multiPreset.GetDefaultPreset(); //Put into no filter preset list m_presetFilterMap[noFilter].insert(preset.m_name); //Put into file mask preset list if any for (const PlatformName& filemask : preset.m_fileMasks) { if (filemask.empty() || filemask[0] != FileMaskDelimiter) { AZ_Warning(LogWindow, false, "File mask '%s' is invalid. It must start with '%c'.", filemask.c_str(), FileMaskDelimiter); continue; } else if (filemask.size() < 2) { AZ_Warning(LogWindow, false, "File mask '%s' is invalid. The '%c' must be followed by at least one other character.", filemask.c_str()); continue; } else if (filemask.find(FileMaskDelimiter, 1) != AZStd::string::npos) { AZ_Warning(LogWindow, false, "File mask '%s' is invalid. It must contain only a single '%c' character.", filemask.c_str(), FileMaskDelimiter); continue; } extraString += (filemask + preset.m_name.GetCStr()); m_presetFilterMap[filemask].insert(preset.m_name); } } if (!extraString.empty()) { AZ::u64 hash = AZStd::hash{}(extraString); m_analysisFingerprint += AZStd::string::format("%llX", hash); } } void BuilderSettingManager::MetafilePathFromImagePath(AZStd::string_view imagePath, AZStd::string& metafilePath) { // Determine if we have a meta file AZ::IO::LocalFileIO fileIO; AZStd::string settingFilePath = AZStd::string::format("%.*s%s", aznumeric_cast(imagePath.size()), imagePath.data(), TextureSettings::ExtensionName); if (fileIO.Exists(settingFilePath.c_str())) { metafilePath = settingFilePath; return; } metafilePath = AZStd::string(); } AZStd::string GetFileMask(AZStd::string_view imageFilePath) { //get file name AZStd::string fileName; QString lowerFileName = imageFilePath.data(); lowerFileName = lowerFileName.toLower(); AzFramework::StringFunc::Path::GetFileName(lowerFileName.toUtf8().constData(), fileName); //get the substring from last '_' size_t lastUnderScore = fileName.find_last_of(FileMaskDelimiter); if (lastUnderScore != AZStd::string::npos) { return fileName.substr(lastUnderScore); } return AZStd::string(); } bool BuilderSettingManager::IsValidPreset(PresetName presetName) const { if (presetName.IsEmpty()) { return false; } return m_presets.find(presetName) != m_presets.end(); } PresetName BuilderSettingManager::GetSuggestedPreset(AZStd::string_view imageFilePath) const { PresetName emptyPreset; //get file mask of this image file AZStd::string fileMask = GetFileMask(imageFilePath); PresetName outPreset = emptyPreset; //use the preset filter map to find if (outPreset.IsEmpty() && !fileMask.empty()) { auto& presetFilterMap = GetPresetFilterMap(); if (presetFilterMap.find(fileMask) != presetFilterMap.end()) { outPreset = *(presetFilterMap.find(fileMask)->second.begin()); } } if (outPreset == emptyPreset) { outPreset = m_defaultPreset; } return outPreset; } AZStd::vector BuilderSettingManager::GetPossiblePresetPaths(const PresetName& presetName) const { AZStd::vector paths; AZStd::string presetFile = AZStd::string::format("%s.preset", presetName.GetCStr()); paths.push_back((m_defaultConfigFolder / presetFile).c_str()); paths.push_back((m_projectConfigFolder / presetFile).c_str()); return paths; } bool BuilderSettingManager::DoesSupportPlatform(AZStd::string_view platformId) { bool rv = m_builderSettings.find(platformId) != m_builderSettings.end(); return rv; } void BuilderSettingManager::SavePresets(AZStd::string_view outputFolder) { for (const auto& element : m_presets) { const PresetEntry& presetEntry = element.second; AZStd::string fileName = AZStd::string::format("%s.preset", presetEntry.m_multiPreset.GetDefaultPreset().m_name.GetCStr()); AZStd::string filePath; if (!AzFramework::StringFunc::Path::Join(outputFolder.data(), fileName.c_str(), filePath)) { AZ_Warning(LogWindow, false, "Failed to construct path with folder '%.*s' and file: '%s' to save preset", aznumeric_cast(outputFolder.size()), outputFolder.data(), filePath.c_str()); continue; } auto result = AZ::JsonSerializationUtils::SaveObjectToFile(&presetEntry.m_multiPreset, filePath); if (!result.IsSuccess()) { AZ_Warning(LogWindow, false, "Failed to save preset '%s' to file '%s'. Error: %s", presetEntry.m_multiPreset.GetDefaultPreset().m_name.GetCStr(), filePath.c_str(), result.GetError().c_str()); } } } void BuilderSettingManager::OnFileChanged(const QString &path) { // handles preset file change // Note: this signal only works with AP but not AssetBuilder AZ_TracePrintf(LogWindow, "File changed %s\n", path.toUtf8().data()); QFileInfo info(path); // skip if the file is not a preset file // Note: for .settings file change it's handled when restart AP. if (info.suffix() != s_presetFileExtension) { return; } ReloadPreset(PresetName(info.baseName().toUtf8().data())); } void BuilderSettingManager::OnFolderChanged([[maybe_unused]] const QString &path) { // handles new file added or removed // Note: this signal only works with AP but not AssetBuilder AZ_TracePrintf(LogWindow, "folder changed %s\n", path.toUtf8().data()); AZStd::lock_guard lock(m_presetMapLock); m_presets.clear(); LoadPresets(m_defaultConfigFolder.Native()); LoadPresets(m_projectConfigFolder.Native()); for (auto& preset : m_presets) { m_fileWatcher.data()->addPath(QString(preset.second.m_presetFilePath.c_str())); } } } // namespace ImageProcessingAtom