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;