From d8ea31ca463712fa9d2977807ae0c3b101239fa1 Mon Sep 17 00:00:00 2001 From: iwubcode Date: Sat, 17 May 2025 12:20:33 -0500 Subject: [PATCH] VideoCommon: rename GameTextureAsset into TextureAsset and make it only contain CustomTextureData. Move validation and load logic to individual functions --- Source/Core/DolphinLib.props | 3 +- .../VideoCommon/Assets/CustomAssetLibrary.cpp | 85 ------ .../VideoCommon/Assets/CustomAssetLibrary.h | 12 +- .../Assets/DirectFilesystemAssetLibrary.cpp | 145 +++-------- .../Assets/DirectFilesystemAssetLibrary.h | 7 +- .../Core/VideoCommon/Assets/TextureAsset.cpp | 93 +------ Source/Core/VideoCommon/Assets/TextureAsset.h | 16 +- .../VideoCommon/Assets/TextureAssetUtils.cpp | 244 ++++++++++++++++++ .../VideoCommon/Assets/TextureAssetUtils.h | 22 ++ Source/Core/VideoCommon/CMakeLists.txt | 3 +- .../Runtime/CustomPipeline.h | 2 +- .../Runtime/GraphicsModActionData.h | 2 +- 12 files changed, 337 insertions(+), 297 deletions(-) delete mode 100644 Source/Core/VideoCommon/Assets/CustomAssetLibrary.cpp create mode 100644 Source/Core/VideoCommon/Assets/TextureAssetUtils.cpp create mode 100644 Source/Core/VideoCommon/Assets/TextureAssetUtils.h diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index b1db7f737a..737a2a16d6 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -675,6 +675,7 @@ + @@ -1322,13 +1323,13 @@ - + diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLibrary.cpp b/Source/Core/VideoCommon/Assets/CustomAssetLibrary.cpp deleted file mode 100644 index 887875d2cb..0000000000 --- a/Source/Core/VideoCommon/Assets/CustomAssetLibrary.cpp +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2023 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "VideoCommon/Assets/CustomAssetLibrary.h" - -#include - -#include "Common/Logging/Log.h" -#include "VideoCommon/Assets/TextureAsset.h" - -namespace VideoCommon -{ -CustomAssetLibrary::LoadInfo CustomAssetLibrary::LoadGameTexture(const AssetID& asset_id, - TextureData* data) -{ - const auto load_info = LoadTexture(asset_id, data); - if (load_info.m_bytes_loaded == 0) - return {}; - - if (data->m_type != TextureData::Type::Type_Texture2D) - { - ERROR_LOG_FMT( - VIDEO, - "Custom asset '{}' is not a valid game texture, it is expected to be a 2d texture " - "but was a '{}'.", - asset_id, data->m_type); - return {}; - } - - // Note: 'LoadTexture()' ensures we have a level loaded - for (std::size_t slice_index = 0; slice_index < data->m_texture.m_slices.size(); slice_index++) - { - auto& slice = data->m_texture.m_slices[slice_index]; - const auto& first_mip = slice.m_levels[0]; - - // Verify that each mip level is the correct size (divide by 2 each time). - u32 current_mip_width = first_mip.width; - u32 current_mip_height = first_mip.height; - for (u32 mip_level = 1; mip_level < static_cast(slice.m_levels.size()); mip_level++) - { - if (current_mip_width != 1 || current_mip_height != 1) - { - current_mip_width = std::max(current_mip_width / 2, 1u); - current_mip_height = std::max(current_mip_height / 2, 1u); - - const VideoCommon::CustomTextureData::ArraySlice::Level& level = slice.m_levels[mip_level]; - if (current_mip_width == level.width && current_mip_height == level.height) - continue; - - ERROR_LOG_FMT(VIDEO, - "Invalid custom game texture size {}x{} for texture asset {}. Slice {} with " - "mipmap level {} " - "must be {}x{}.", - level.width, level.height, asset_id, slice_index, mip_level, - current_mip_width, current_mip_height); - } - else - { - // It is invalid to have more than a single 1x1 mipmap. - ERROR_LOG_FMT( - VIDEO, - "Custom game texture {} has too many 1x1 mipmaps for slice {}. Skipping extra levels.", - asset_id, slice_index); - } - - // Drop this mip level and any others after it. - while (slice.m_levels.size() > mip_level) - slice.m_levels.pop_back(); - } - - // All levels have to have the same format. - if (std::ranges::any_of(slice.m_levels, - [&first_mip](const auto& l) { return l.format != first_mip.format; })) - { - ERROR_LOG_FMT( - VIDEO, "Custom game texture {} has inconsistent formats across mip levels for slice {}.", - asset_id, slice_index); - - return {}; - } - } - - return load_info; -} -} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h b/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h index a846b67d27..32fac4fdf6 100644 --- a/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h +++ b/Source/Core/VideoCommon/Assets/CustomAssetLibrary.h @@ -10,10 +10,11 @@ namespace VideoCommon { +class CustomTextureData; struct MaterialData; struct MeshData; struct PixelShaderData; -struct TextureData; +struct TextureAndSamplerData; // This class provides functionality to load // specific data (like textures). Where this data @@ -31,12 +32,11 @@ public: virtual ~CustomAssetLibrary() = default; - // Loads a texture, if there are no levels, bytes loaded will be empty - virtual LoadInfo LoadTexture(const AssetID& asset_id, TextureData* data) = 0; + // Loads a texture with a sampler and type, if there are no levels, bytes loaded will be empty + virtual LoadInfo LoadTexture(const AssetID& asset_id, TextureAndSamplerData* data) = 0; - // Loads a texture as a game texture, providing additional checks like confirming - // each mip level size is correct and that the format is consistent across the data - LoadInfo LoadGameTexture(const AssetID& asset_id, TextureData* data); + // Loads a texture, if there are no levels, bytes loaded will be empty + virtual LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) = 0; // Loads a pixel shader virtual LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) = 0; diff --git a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp index 6d67269518..5734da4961 100644 --- a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp +++ b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp @@ -17,6 +17,7 @@ #include "VideoCommon/Assets/MeshAsset.h" #include "VideoCommon/Assets/ShaderAsset.h" #include "VideoCommon/Assets/TextureAsset.h" +#include "VideoCommon/Assets/TextureAssetUtils.h" #include "VideoCommon/RenderState.h" namespace VideoCommon @@ -277,7 +278,37 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMesh(const AssetI } CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id, - TextureData* data) + CustomTextureData* data) +{ + const auto asset_map = GetAssetMapForID(asset_id); + if (asset_map.empty()) + { + ERROR_LOG_FMT(VIDEO, "Asset '{}' error - raw texture expected to have one or two files mapped!", + asset_id); + return {}; + } + + const auto texture_path = asset_map.find("texture"); + + if (texture_path == asset_map.end()) + { + ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a texture entry mapped!", asset_id); + return {}; + } + + if (!LoadTextureDataFromFile(asset_id, texture_path->second, + TextureAndSamplerData::Type::Type_Texture2D, data)) + { + return {}; + } + if (!PurgeInvalidMipsFromTextureData(asset_id, data)) + return {}; + + return LoadInfo{GetAssetSize(*data)}; +} + +CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id, + TextureAndSamplerData* data) { const auto asset_map = GetAssetMapForID(asset_id); @@ -330,7 +361,7 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass } const auto& root_obj = root.get(); - if (!TextureData::FromJson(asset_id, root_obj, data)) + if (!TextureAndSamplerData::FromJson(asset_id, root_obj, data)) { return {}; } @@ -338,61 +369,15 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass else { data->m_sampler = RenderState::GetLinearSamplerState(); - data->m_type = TextureData::Type::Type_Texture2D; + data->m_type = TextureAndSamplerData::Type::Type_Texture2D; } - auto ext = PathToString(texture_path->second.extension()); - Common::ToLower(&ext); - if (ext == ".dds") - { - if (!LoadDDSTexture(&data->m_texture, PathToString(texture_path->second))) - { - ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load dds texture!", asset_id); - return {}; - } + if (!LoadTextureDataFromFile(asset_id, texture_path->second, data->m_type, &data->m_texture)) + return {}; + if (!PurgeInvalidMipsFromTextureData(asset_id, &data->m_texture)) + return {}; - if (data->m_texture.m_slices.empty()) [[unlikely]] - data->m_texture.m_slices.push_back({}); - - if (!LoadMips(texture_path->second, &data->m_texture.m_slices[0])) - return {}; - - return LoadInfo{GetAssetSize(data->m_texture) + metadata_size}; - } - else if (ext == ".png") - { - // PNG could support more complicated texture types in the future - // but for now just error - if (data->m_type != TextureData::Type::Type_Texture2D) - { - ERROR_LOG_FMT(VIDEO, "Asset '{}' error - PNG is not supported for texture type '{}'!", - asset_id, data->m_type); - return {}; - } - - // If we have no slices, create one - if (data->m_texture.m_slices.empty()) - data->m_texture.m_slices.push_back({}); - - auto& slice = data->m_texture.m_slices[0]; - // If we have no levels, create one to pass into LoadPNGTexture - if (slice.m_levels.empty()) - slice.m_levels.push_back({}); - - if (!LoadPNGTexture(&slice.m_levels[0], PathToString(texture_path->second))) - { - ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load png texture!", asset_id); - return {}; - } - - if (!LoadMips(texture_path->second, &slice)) - return {}; - - return LoadInfo{GetAssetSize(data->m_texture) + metadata_size}; - } - - ERROR_LOG_FMT(VIDEO, "Asset '{}' error - extension '{}' unknown!", asset_id, ext); - return {}; + return LoadInfo{GetAssetSize(data->m_texture) + metadata_size}; } void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id, @@ -402,58 +387,6 @@ void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id, m_asset_id_to_asset_map_path[asset_id] = std::move(asset_path_map); } -bool DirectFilesystemAssetLibrary::LoadMips(const std::filesystem::path& asset_path, - CustomTextureData::ArraySlice* data) -{ - if (!data) [[unlikely]] - return false; - - std::string path; - std::string filename; - std::string extension; - SplitPath(PathToString(asset_path), &path, &filename, &extension); - - std::string extension_lower = extension; - Common::ToLower(&extension_lower); - - // Load additional mip levels - for (u32 mip_level = static_cast(data->m_levels.size());; mip_level++) - { - const auto mip_level_filename = filename + fmt::format("_mip{}", mip_level); - - const auto full_path = path + mip_level_filename + extension; - if (!File::Exists(full_path)) - return true; - - VideoCommon::CustomTextureData::ArraySlice::Level level; - if (extension_lower == ".dds") - { - if (!LoadDDSTexture(&level, full_path, mip_level)) - { - ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename); - return false; - } - } - else if (extension_lower == ".png") - { - if (!LoadPNGTexture(&level, full_path)) - { - ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename); - return false; - } - } - else - { - ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' has unsupported extension", mip_level_filename); - return false; - } - - data->m_levels.push_back(std::move(level)); - } - - return true; -} - VideoCommon::Assets::AssetMap DirectFilesystemAssetLibrary::GetAssetMapForID(const AssetID& asset_id) const { diff --git a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h index 97f6118969..a3dede8722 100644 --- a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h +++ b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.h @@ -10,6 +10,7 @@ #include "VideoCommon/Assets/CustomAssetLibrary.h" #include "VideoCommon/Assets/CustomTextureData.h" +#include "VideoCommon/Assets/TextureAsset.h" #include "VideoCommon/Assets/Types.h" namespace VideoCommon @@ -19,7 +20,8 @@ namespace VideoCommon class DirectFilesystemAssetLibrary final : public CustomAssetLibrary { public: - LoadInfo LoadTexture(const AssetID& asset_id, TextureData* data) override; + LoadInfo LoadTexture(const AssetID& asset_id, TextureAndSamplerData* data) override; + LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) override; LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) override; LoadInfo LoadMaterial(const AssetID& asset_id, MaterialData* data) override; LoadInfo LoadMesh(const AssetID& asset_id, MeshData* data) override; @@ -30,9 +32,6 @@ public: void SetAssetIDMapData(const AssetID& asset_id, Assets::AssetMap asset_path_map); private: - // Loads additional mip levels into the texture structure until _mip texture is not found - bool LoadMips(const std::filesystem::path& asset_path, CustomTextureData::ArraySlice* data); - // Gets the asset map given an asset id Assets::AssetMap GetAssetMapForID(const AssetID& asset_id) const; diff --git a/Source/Core/VideoCommon/Assets/TextureAsset.cpp b/Source/Core/VideoCommon/Assets/TextureAsset.cpp index 335ed73b33..0b3deecca7 100644 --- a/Source/Core/VideoCommon/Assets/TextureAsset.cpp +++ b/Source/Core/VideoCommon/Assets/TextureAsset.cpp @@ -153,8 +153,8 @@ bool ParseSampler(const VideoCommon::CustomAssetLibrary::AssetID& asset_id, return true; } } // namespace -bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id, - const picojson::object& json, TextureData* data) +bool TextureAndSamplerData::FromJson(const CustomAssetLibrary::AssetID& asset_id, + const picojson::object& json, TextureAndSamplerData* data) { const auto type_iter = json.find("type"); if (type_iter == json.end()) @@ -176,7 +176,7 @@ bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id, if (type == "texture2d") { - data->m_type = TextureData::Type::Type_Texture2D; + data->m_type = TextureAndSamplerData::Type::Type_Texture2D; if (!ParseSampler(asset_id, json, &data->m_sampler)) { @@ -185,7 +185,7 @@ bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id, } else if (type == "texturecube") { - data->m_type = TextureData::Type::Type_TextureCube; + data->m_type = TextureAndSamplerData::Type::Type_TextureCube; } else { @@ -199,7 +199,7 @@ bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id, return true; } -void TextureData::ToJson(picojson::object* obj, const TextureData& data) +void TextureAndSamplerData::ToJson(picojson::object* obj, const TextureAndSamplerData& data) { if (!obj) [[unlikely]] return; @@ -207,13 +207,13 @@ void TextureData::ToJson(picojson::object* obj, const TextureData& data) auto& json_obj = *obj; switch (data.m_type) { - case TextureData::Type::Type_Texture2D: + case TextureAndSamplerData::Type::Type_Texture2D: json_obj.emplace("type", "texture2d"); break; - case TextureData::Type::Type_TextureCube: + case TextureAndSamplerData::Type::Type_TextureCube: json_obj.emplace("type", "texturecube"); break; - case TextureData::Type::Type_Undefined: + case TextureAndSamplerData::Type::Type_Undefined: break; }; @@ -254,10 +254,10 @@ void TextureData::ToJson(picojson::object* obj, const TextureData& data) json_obj.emplace("filter_mode", filter_mode); } -CustomAssetLibrary::LoadInfo GameTextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id) +CustomAssetLibrary::LoadInfo TextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id) { - auto potential_data = std::make_shared(); - const auto loaded_info = m_owning_library->LoadGameTexture(asset_id, potential_data.get()); + auto potential_data = std::make_shared(); + const auto loaded_info = m_owning_library->LoadTexture(asset_id, potential_data.get()); if (loaded_info.m_bytes_loaded == 0) return {}; { @@ -267,75 +267,4 @@ CustomAssetLibrary::LoadInfo GameTextureAsset::LoadImpl(const CustomAssetLibrary } return loaded_info; } - -bool GameTextureAsset::Validate(u32 native_width, u32 native_height) const -{ - std::lock_guard lk(m_data_lock); - - if (!m_loaded) - { - ERROR_LOG_FMT(VIDEO, - "Game texture can't be validated for asset '{}' because it is not loaded yet.", - GetAssetId()); - return false; - } - - if (m_data->m_texture.m_slices.empty()) - { - ERROR_LOG_FMT(VIDEO, - "Game texture can't be validated for asset '{}' because no data was available.", - GetAssetId()); - return false; - } - - if (m_data->m_texture.m_slices.size() > 1) - { - ERROR_LOG_FMT( - VIDEO, - "Game texture can't be validated for asset '{}' because it has more slices than expected.", - GetAssetId()); - return false; - } - - const auto& slice = m_data->m_texture.m_slices[0]; - if (slice.m_levels.empty()) - { - ERROR_LOG_FMT( - VIDEO, - "Game texture can't be validated for asset '{}' because first slice has no data available.", - GetAssetId()); - return false; - } - - // Verify that the aspect ratio of the texture hasn't changed, as this could have - // side-effects. - const VideoCommon::CustomTextureData::ArraySlice::Level& first_mip = slice.m_levels[0]; - if (first_mip.width * native_height != first_mip.height * native_width) - { - // Note: this feels like this should return an error but - // for legacy reasons this is only a notice that something *could* - // go wrong - WARN_LOG_FMT( - VIDEO, - "Invalid custom texture size {}x{} for game texture asset '{}'. The aspect differs " - "from the native size {}x{}.", - first_mip.width, first_mip.height, GetAssetId(), native_width, native_height); - } - - // Same deal if the custom texture isn't a multiple of the native size. - if (native_width != 0 && native_height != 0 && - (first_mip.width % native_width || first_mip.height % native_height)) - { - // Note: this feels like this should return an error but - // for legacy reasons this is only a notice that something *could* - // go wrong - WARN_LOG_FMT( - VIDEO, - "Invalid custom texture size {}x{} for game texture asset '{}'. Please use an integer " - "upscaling factor based on the native size {}x{}.", - first_mip.width, first_mip.height, GetAssetId(), native_width, native_height); - } - - return true; -} } // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/TextureAsset.h b/Source/Core/VideoCommon/Assets/TextureAsset.h index e0929a79f0..f83d8ad109 100644 --- a/Source/Core/VideoCommon/Assets/TextureAsset.h +++ b/Source/Core/VideoCommon/Assets/TextureAsset.h @@ -13,11 +13,11 @@ namespace VideoCommon { -struct TextureData +struct TextureAndSamplerData { static bool FromJson(const CustomAssetLibrary::AssetID& asset_id, const picojson::object& json, - TextureData* data); - static void ToJson(picojson::object* obj, const TextureData& data); + TextureAndSamplerData* data); + static void ToJson(picojson::object* obj, const TextureAndSamplerData& data); enum class Type { Type_Undefined, @@ -30,23 +30,19 @@ struct TextureData SamplerState m_sampler; }; -class GameTextureAsset final : public CustomLoadableAsset +class TextureAsset final : public CustomLoadableAsset { public: using CustomLoadableAsset::CustomLoadableAsset; - // Validates that the game texture matches the native dimensions provided - // Callees are expected to call this once the data is loaded - bool Validate(u32 native_width, u32 native_height) const; - private: CustomAssetLibrary::LoadInfo LoadImpl(const CustomAssetLibrary::AssetID& asset_id) override; }; } // namespace VideoCommon template <> -struct fmt::formatter - : EnumFormatter +struct fmt::formatter + : EnumFormatter { constexpr formatter() : EnumFormatter({"Undefined", "Texture2D", "TextureCube"}) {} }; diff --git a/Source/Core/VideoCommon/Assets/TextureAssetUtils.cpp b/Source/Core/VideoCommon/Assets/TextureAssetUtils.cpp new file mode 100644 index 0000000000..5285c61bba --- /dev/null +++ b/Source/Core/VideoCommon/Assets/TextureAssetUtils.cpp @@ -0,0 +1,244 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "VideoCommon/Assets/TextureAssetUtils.h" + +#include + +#include "Common/FileUtil.h" +#include "Common/Logging/Log.h" +#include "Common/StringUtil.h" + +namespace VideoCommon +{ +namespace +{ +// Loads additional mip levels into the texture structure until _mip texture is not found +bool LoadMips(const std::filesystem::path& asset_path, CustomTextureData::ArraySlice* data) +{ + if (!data) [[unlikely]] + return false; + + std::string path; + std::string filename; + std::string extension; + SplitPath(PathToString(asset_path), &path, &filename, &extension); + + std::string extension_lower = extension; + Common::ToLower(&extension_lower); + + // Load additional mip levels + for (u32 mip_level = static_cast(data->m_levels.size());; mip_level++) + { + const auto mip_level_filename = filename + fmt::format("_mip{}", mip_level); + + const auto full_path = path + mip_level_filename + extension; + if (!File::Exists(full_path)) + return true; + + VideoCommon::CustomTextureData::ArraySlice::Level level; + if (extension_lower == ".dds") + { + if (!LoadDDSTexture(&level, full_path, mip_level)) + { + ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename); + return false; + } + } + else if (extension_lower == ".png") + { + if (!LoadPNGTexture(&level, full_path)) + { + ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename); + return false; + } + } + else + { + ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' has unsupported extension", mip_level_filename); + return false; + } + + data->m_levels.push_back(std::move(level)); + } + + return true; +} +} // namespace +bool LoadTextureDataFromFile(const CustomAssetLibrary::AssetID& asset_id, + const std::filesystem::path& asset_path, + TextureAndSamplerData::Type type, CustomTextureData* data) +{ + auto ext = PathToString(asset_path.extension()); + Common::ToLower(&ext); + if (ext == ".dds") + { + if (!LoadDDSTexture(data, PathToString(asset_path))) + { + ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load dds texture!", asset_id); + return false; + } + + if (data->m_slices.empty()) [[unlikely]] + data->m_slices.emplace_back(); + + if (!LoadMips(asset_path, data->m_slices.data())) + return false; + + return true; + } + + if (ext == ".png") + { + // PNG could support more complicated texture types in the future + // but for now just error + if (type != TextureAndSamplerData::Type::Type_Texture2D) + { + ERROR_LOG_FMT(VIDEO, "Asset '{}' error - PNG is not supported for texture type '{}'!", + asset_id, type); + return {}; + } + + // If we have no slices, create one + if (data->m_slices.empty()) + data->m_slices.emplace_back(); + + auto& slice = data->m_slices[0]; + // If we have no levels, create one to pass into LoadPNGTexture + if (slice.m_levels.empty()) + slice.m_levels.emplace_back(); + + if (!LoadPNGTexture(slice.m_levels.data(), PathToString(asset_path))) + { + ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load png texture!", asset_id); + return false; + } + + if (!LoadMips(asset_path, &slice)) + return false; + + return true; + } + + ERROR_LOG_FMT(VIDEO, "Asset '{}' error - extension '{}' unknown!", asset_id, ext); + return false; +} + +bool ValidateTextureData(const CustomAssetLibrary::AssetID& asset_id, const CustomTextureData& data, + u32 native_width, u32 native_height) +{ + if (data.m_slices.empty()) + { + ERROR_LOG_FMT(VIDEO, + "Texture data can't be validated for asset '{}' because no data was available.", + asset_id); + return false; + } + + if (data.m_slices.size() > 1) + { + ERROR_LOG_FMT( + VIDEO, + "Texture data can't be validated for asset '{}' because it has more slices than expected.", + asset_id); + return false; + } + + const auto& slice = data.m_slices[0]; + if (slice.m_levels.empty()) + { + ERROR_LOG_FMT( + VIDEO, + "Texture data can't be validated for asset '{}' because first slice has no data available.", + asset_id); + return false; + } + + // Verify that the aspect ratio of the texture hasn't changed, as this could have + // side-effects. + const CustomTextureData::ArraySlice::Level& first_mip = slice.m_levels[0]; + if (first_mip.width * native_height != first_mip.height * native_width) + { + // Note: this feels like this should return an error but + // for legacy reasons this is only a notice that something *could* + // go wrong + WARN_LOG_FMT(VIDEO, + "Invalid texture data size {}x{} for asset '{}'. The aspect differs " + "from the native size {}x{}.", + first_mip.width, first_mip.height, asset_id, native_width, native_height); + } + + // Same deal if the custom texture isn't a multiple of the native size. + if (native_width != 0 && native_height != 0 && + (first_mip.width % native_width || first_mip.height % native_height)) + { + // Note: this feels like this should return an error but + // for legacy reasons this is only a notice that something *could* + // go wrong + WARN_LOG_FMT(VIDEO, + "Invalid texture data size {}x{} for asset '{}'. Please use an integer " + "upscaling factor based on the native size {}x{}.", + first_mip.width, first_mip.height, asset_id, native_width, native_height); + } + + return true; +} + +bool PurgeInvalidMipsFromTextureData(const CustomAssetLibrary::AssetID& asset_id, + CustomTextureData* data) +{ + for (std::size_t slice_index = 0; slice_index < data->m_slices.size(); slice_index++) + { + auto& slice = data->m_slices[slice_index]; + const auto& first_mip = slice.m_levels[0]; + + // Verify that each mip level is the correct size (divide by 2 each time). + u32 current_mip_width = first_mip.width; + u32 current_mip_height = first_mip.height; + for (u32 mip_level = 1; mip_level < static_cast(slice.m_levels.size()); mip_level++) + { + if (current_mip_width != 1 || current_mip_height != 1) + { + current_mip_width = std::max(current_mip_width / 2, 1u); + current_mip_height = std::max(current_mip_height / 2, 1u); + + const VideoCommon::CustomTextureData::ArraySlice::Level& level = slice.m_levels[mip_level]; + if (current_mip_width == level.width && current_mip_height == level.height) + continue; + + ERROR_LOG_FMT(VIDEO, + "Invalid custom game texture size {}x{} for texture asset {}. Slice {} with " + "mipmap level {} " + "must be {}x{}.", + level.width, level.height, asset_id, slice_index, mip_level, + current_mip_width, current_mip_height); + } + else + { + // It is invalid to have more than a single 1x1 mipmap. + ERROR_LOG_FMT(VIDEO, + "Custom game texture {} has too many 1x1 mipmaps for slice {}. Skipping " + "extra levels.", + asset_id, slice_index); + } + + // Drop this mip level and any others after it. + while (slice.m_levels.size() > mip_level) + slice.m_levels.pop_back(); + } + + // All levels have to have the same format. + if (std::ranges::any_of(slice.m_levels, + [&first_mip](const auto& l) { return l.format != first_mip.format; })) + { + ERROR_LOG_FMT( + VIDEO, "Custom game texture {} has inconsistent formats across mip levels for slice {}.", + asset_id, slice_index); + + return false; + } + } + + return true; +} +} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/TextureAssetUtils.h b/Source/Core/VideoCommon/Assets/TextureAssetUtils.h new file mode 100644 index 0000000000..ef7bc00751 --- /dev/null +++ b/Source/Core/VideoCommon/Assets/TextureAssetUtils.h @@ -0,0 +1,22 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "VideoCommon/Assets/CustomAssetLibrary.h" +#include "VideoCommon/Assets/TextureAsset.h" + +namespace VideoCommon +{ +bool LoadTextureDataFromFile(const CustomAssetLibrary::AssetID& asset_id, + const std::filesystem::path& asset_path, + TextureAndSamplerData::Type type, CustomTextureData* data); + +bool ValidateTextureData(const CustomAssetLibrary::AssetID& asset_id, const CustomTextureData& data, + u32 native_width, u32 native_height); + +bool PurgeInvalidMipsFromTextureData(const CustomAssetLibrary::AssetID& asset_id, + CustomTextureData* data); +} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt index b449903249..b712599ebc 100644 --- a/Source/Core/VideoCommon/CMakeLists.txt +++ b/Source/Core/VideoCommon/CMakeLists.txt @@ -10,7 +10,6 @@ add_library(videocommon AbstractTexture.h Assets/CustomAsset.cpp Assets/CustomAsset.h - Assets/CustomAssetLibrary.cpp Assets/CustomAssetLibrary.h Assets/CustomAssetLoader.cpp Assets/CustomAssetLoader.h @@ -26,6 +25,8 @@ add_library(videocommon Assets/ShaderAsset.h Assets/TextureAsset.cpp Assets/TextureAsset.h + Assets/TextureAssetUtils.cpp + Assets/TextureAssetUtils.h Assets/Types.h AsyncRequests.cpp AsyncRequests.h diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/CustomPipeline.h b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/CustomPipeline.h index ea840ba911..3a904e0858 100644 --- a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/CustomPipeline.h +++ b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/CustomPipeline.h @@ -28,7 +28,7 @@ struct CustomPipeline struct CachedTextureAsset { - VideoCommon::CachedAsset m_cached_asset; + VideoCommon::CachedAsset m_cached_asset; std::unique_ptr m_texture; std::string m_sampler_code; std::string m_define_code; diff --git a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionData.h b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionData.h index cecd0ce94f..f7c12fe6e5 100644 --- a/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionData.h +++ b/Source/Core/VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionData.h @@ -47,7 +47,7 @@ struct TextureCreate std::string_view texture_name; u32 texture_width; u32 texture_height; - std::vector>* custom_textures; + std::vector>* custom_textures; // Dependencies needed to reload the texture and trigger this create again std::vector>* additional_dependencies;