/* * 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 namespace ImageProcessingAtom { const char* BuilderSettingManager::s_defaultConfigRelativeFolder = "Gems/Atom/Asset/ImageProcessingAtom/Config/"; const char* BuilderSettingManager::s_projectConfigRelativeFolder = "Config/AtomImageBuilder/"; const char* BuilderSettingManager::s_builderSettingFileName = "ImageBuilder.settings"; const char* BuilderSettingManager::s_presetFileExtension = ".preset"; const char FileMaskDelimiter = '_'; #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(1) ->Field("AnalysisFingerprint", &BuilderSettingManager::m_analysisFingerprint) ->Field("BuildSettings", &BuilderSettingManager::m_builderSettings) ->Field("DefaultPresetsByFileMask", &BuilderSettingManager::m_defaultPresetByFileMask) ->Field("DefaultPreset", &BuilderSettingManager::m_defaultPreset) ->Field("DefaultPresetAlpha", &BuilderSettingManager::m_defaultPresetAlpha) ->Field("DefaultPresetNonePOT", &BuilderSettingManager::m_defaultPresetNonePOT); } } 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) { 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; } const PresetSettings* BuilderSettingManager::GetPreset(const AZ::Uuid presetId, const PlatformName& platform, AZStd::string_view* settingsFilePathOut) { AZStd::lock_guard lock(m_presetMapLock); for (const auto& namePreset : m_presets) { if (namePreset.second.m_multiPreset.GetPresetId() == presetId) { if (settingsFilePathOut) { *settingsFilePathOut = namePreset.second.m_presetFilePath; } return namePreset.second.m_multiPreset.GetPreset(platform); } } return nullptr; } const BuilderSettings* BuilderSettingManager::GetBuilderSetting(const PlatformName& platform) { if (m_builderSettings.find(platform) != m_builderSettings.end()) { return &m_builderSettings[platform]; } return nullptr; } const PlatformNameList BuilderSettingManager::GetPlatformList() { PlatformNameList platforms; for (auto& builderSetting : m_builderSettings) { if (builderSetting.second.m_enablePlatform) { platforms.push_back(builderSetting.first); } } return platforms; } const AZStd::map >& BuilderSettingManager::GetPresetFilterMap() { AZStd::lock_guard lock(m_presetMapLock); return m_presetFilterMap; } const AZ::Uuid BuilderSettingManager::GetPresetIdFromName(const PresetName& presetName) { AZStd::lock_guard lock(m_presetMapLock); auto itr = m_presets.find(presetName); if (itr != m_presets.end()) { return itr->second.m_multiPreset.GetPresetId(); } return AZ::Uuid::CreateNull(); } 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 "Unknown"; } void BuilderSettingManager::ClearSettings() { AZStd::lock_guard lock(m_presetMapLock); m_presetFilterMap.clear(); m_builderSettings.clear(); m_presets.clear(); m_defaultPresetByFileMask.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")); } // Construct the default setting path AZ::IO::FixedMaxPath defaultConfigFolder; if (auto engineRoot = fileIoBase->ResolvePath("@engroot@"); engineRoot.has_value()) { defaultConfigFolder = *engineRoot; defaultConfigFolder /= s_defaultConfigRelativeFolder; } AZ::IO::FixedMaxPath projectConfigFolder; if (auto sourceGameRoot = fileIoBase->ResolvePath("@devassets@"); sourceGameRoot.has_value()) { projectConfigFolder = *sourceGameRoot; projectConfigFolder /= s_projectConfigRelativeFolder; } AZStd::lock_guard lock(m_presetMapLock); ClearSettings(); outcome = LoadSettings((projectConfigFolder / s_builderSettingFileName).Native()); if (!outcome.IsSuccess()) { outcome = LoadSettings((defaultConfigFolder / s_builderSettingFileName).Native()); } 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(defaultConfigFolder.Native()); LoadPresets(projectConfigFolder.Native()); // Regenerate file mask mapping after all presets loaded RegenerateMappings(); } return outcome; } void BuilderSettingManager::LoadPresets(AZStd::string_view presetFolder) { AZStd::lock_guard lock(m_presetMapLock); QDirIterator it(presetFolder.data(), QStringList() << "*.preset", QDir::Files, QDirIterator::NoIteratorFlags); while (it.hasNext()) { QString filePath = it.next(); QFileInfo fileInfo = it.fileInfo(); MultiplatformPresetSettings preset; auto result = AZ::JsonSerializationUtils::LoadObjectFromFile(preset, filePath.toUtf8().data()); if (!result.IsSuccess()) { AZ_Warning("Image Processing", false, "Failed to load preset file %s. Error: %s", filePath.toUtf8().data(), result.GetError().c_str()); } PresetName presetName(fileInfo.baseName().toUtf8().data()); AZ_Warning("Image Processing", presetName == preset.GetPresetName(), "Preset file name '%s' is not" " same as preset name '%s'. Using preset file name as preset name", filePath.toUtf8().data(), preset.GetPresetName().c_str()); preset.SetPresetName(presetName); m_presets[presetName] = PresetEntry{preset, filePath.toUtf8().data()}; } } StringOutcome BuilderSettingManager::LoadConfigFromFolder(AZStd::string_view configFolder) { // 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); RegenerateMappings(); } return result; } 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::RegenerateMappings() { AZStd::lock_guard lock(m_presetMapLock); AZStd::string noFilter = AZStd::string(); m_presetFilterMap.clear(); 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("Image Processing", false, "File mask '%s' is invalid. It must start with '%c'.", filemask.c_str(), FileMaskDelimiter); continue; } else if (filemask.size() < 2) { AZ_Warning("Image Processing", 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("Image Processing", false, "File mask '%s' is invalid. It must contain only a single '%c' character.", filemask.c_str(), FileMaskDelimiter); continue; } m_presetFilterMap[filemask].insert(preset.m_name); } } } 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(); } AZ::Uuid BuilderSettingManager::GetSuggestedPreset(AZStd::string_view imageFilePath, IImageObjectPtr imageFromFile) { //load the image to get its size for later use IImageObjectPtr image = imageFromFile; //if the input image is empty we will try to load it from the path if (imageFromFile == nullptr) { image = IImageObjectPtr(LoadImageFromFile(imageFilePath)); } if (image == nullptr) { return AZ::Uuid::CreateNull(); } //get file mask of this image file AZStd::string fileMask = GetFileMask(imageFilePath); AZ::Uuid outPreset = AZ::Uuid::CreateNull(); //check default presets for some file masks if (m_defaultPresetByFileMask.find(fileMask) != m_defaultPresetByFileMask.end()) { outPreset = m_defaultPresetByFileMask[fileMask]; } //use the preset filter map to find if (outPreset.IsNull() && !fileMask.empty()) { auto& presetFilterMap = GetPresetFilterMap(); if (presetFilterMap.find(fileMask) != presetFilterMap.end()) { AZStd::string presetName = *(presetFilterMap.find(fileMask)->second.begin()); outPreset = GetPresetIdFromName(presetName); } } const PresetSettings* presetInfo = nullptr; if (!outPreset.IsNull()) { presetInfo = GetPreset(outPreset); //special case for cubemap if (presetInfo && presetInfo->m_cubemapSetting) { // If it's not a latitude-longitude map or it doesn't match any cubemap layouts then reset its preset if (!IsValidLatLongMap(image) && CubemapLayout::GetCubemapLayoutInfo(image) == nullptr) { outPreset = AZ::Uuid::CreateNull(); } } } if (outPreset.IsNull()) { if (image->GetAlphaContent() == EAlphaContent::eAlphaContent_Absent) { outPreset = m_defaultPreset; } else { outPreset = m_defaultPresetAlpha; } } //get the pixel format for selected preset presetInfo = GetPreset(outPreset); if (presetInfo) { //valid whether image size work with pixel format if (CPixelFormats::GetInstance().IsImageSizeValid(presetInfo->m_pixelFormat, image->GetWidth(0), image->GetHeight(0), false)) { return outPreset; } else { AZ_Warning("Image Processing", false, "Image dimensions are not compatible with preset '%s'. The default preset will be used.", presetInfo->m_name.c_str()); } } //uncompressed one which could be used for almost everything return m_defaultPresetNonePOT; } 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.c_str()); AZStd::string filePath; if (!AzFramework::StringFunc::Path::Join(outputFolder.data(), fileName.c_str(), filePath)) { AZ_Warning("Image Processing", 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("Image Processing", false, "Failed to save preset '%s' to file '%s'. Error: %s", presetEntry.m_multiPreset.GetDefaultPreset().m_name.c_str(), filePath.c_str(), result.GetError().c_str()); } } } } // namespace ImageProcessingAtom