diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 5ae7b6fd67..05142ae672 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -633,8 +633,10 @@ + + @@ -1246,8 +1248,10 @@ + + diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp b/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp new file mode 100644 index 0000000000..d2189e655b --- /dev/null +++ b/Source/Core/VideoCommon/Assets/CustomAssetLoader.cpp @@ -0,0 +1,93 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "VideoCommon/Assets/CustomAssetLoader.h" + +#include "Common/Logging/Log.h" +#include "Common/MemoryUtil.h" +#include "VideoCommon/Assets/CustomAssetLibrary.h" + +namespace VideoCommon +{ +void CustomAssetLoader::Init() +{ + m_asset_monitor_thread_shutdown.Clear(); + + const size_t sys_mem = Common::MemPhysical(); + const size_t recommended_min_mem = 2 * size_t(1024 * 1024 * 1024); + // keep 2GB memory for system stability if system RAM is 4GB+ - use half of memory in other cases + m_max_memory_available = + (sys_mem / 2 < recommended_min_mem) ? (sys_mem / 2) : (sys_mem - recommended_min_mem); + + m_asset_monitor_thread = std::thread([this]() { + Common::SetCurrentThreadName("Asset monitor"); + while (true) + { + if (m_asset_monitor_thread_shutdown.IsSet()) + { + break; + } + + std::this_thread::sleep_for(TIME_BETWEEN_ASSET_MONITOR_CHECKS); + + std::lock_guard lk(m_assets_lock); + for (auto& [asset_id, asset_to_monitor] : m_assets_to_monitor) + { + if (auto ptr = asset_to_monitor.lock()) + { + const auto write_time = ptr->GetLastWriteTime(); + if (write_time > ptr->GetLastLoadedTime()) + { + (void)ptr->Load(); + } + } + } + } + }); + + m_asset_load_thread.Reset("Custom Asset Loader", [this](std::weak_ptr asset) { + if (auto ptr = asset.lock()) + { + if (ptr->Load()) + { + if (m_max_memory_available >= m_total_bytes_loaded + ptr->GetByteSizeInMemory()) + { + m_total_bytes_loaded += ptr->GetByteSizeInMemory(); + + std::lock_guard lk(m_assets_lock); + m_assets_to_monitor.try_emplace(ptr->GetAssetId(), ptr); + } + else + { + ERROR_LOG_FMT(VIDEO, "Failed to load asset {} because there was not enough memory.", + ptr->GetAssetId()); + } + } + } + }); +} + +void CustomAssetLoader ::Shutdown() +{ + m_asset_load_thread.Shutdown(true); + + m_asset_monitor_thread_shutdown.Set(); + m_asset_monitor_thread.join(); + m_assets_to_monitor.clear(); + m_total_bytes_loaded = 0; +} + +std::shared_ptr +CustomAssetLoader::LoadTexture(const CustomAssetLibrary::AssetID& asset_id, + std::shared_ptr library) +{ + return LoadOrCreateAsset(asset_id, m_textures, std::move(library)); +} + +std::shared_ptr +CustomAssetLoader::LoadGameTexture(const CustomAssetLibrary::AssetID& asset_id, + std::shared_ptr library) +{ + return LoadOrCreateAsset(asset_id, m_game_textures, std::move(library)); +} +} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/CustomAssetLoader.h b/Source/Core/VideoCommon/Assets/CustomAssetLoader.h new file mode 100644 index 0000000000..da1f54b133 --- /dev/null +++ b/Source/Core/VideoCommon/Assets/CustomAssetLoader.h @@ -0,0 +1,81 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "Common/Flag.h" +#include "Common/WorkQueueThread.h" +#include "VideoCommon/Assets/CustomAsset.h" +#include "VideoCommon/Assets/TextureAsset.h" + +namespace VideoCommon +{ +// This class is responsible for loading data asynchronously when requested +// and watches that data asynchronously reloading it if it changes +class CustomAssetLoader +{ +public: + CustomAssetLoader() = default; + ~CustomAssetLoader() = default; + CustomAssetLoader(const CustomAssetLoader&) = delete; + CustomAssetLoader(CustomAssetLoader&&) = delete; + CustomAssetLoader& operator=(const CustomAssetLoader&) = delete; + CustomAssetLoader& operator=(CustomAssetLoader&&) = delete; + + void Init(); + void Shutdown(); + + // The following Load* functions will load or create an asset associated + // with the given asset id + // Loads happen asynchronously where the data will be set now or in the future + // Callees are expected to query the underlying data with 'GetData()' + // from the 'CustomLoadableAsset' class to determine if the data is ready for use + std::shared_ptr LoadTexture(const CustomAssetLibrary::AssetID& asset_id, + std::shared_ptr library); + + std::shared_ptr LoadGameTexture(const CustomAssetLibrary::AssetID& asset_id, + std::shared_ptr library); + +private: + // TODO C++20: use a 'derived_from' concept against 'CustomAsset' when available + template + std::shared_ptr + LoadOrCreateAsset(const CustomAssetLibrary::AssetID& asset_id, + std::map>& asset_map, + std::shared_ptr library) + { + auto [it, inserted] = asset_map.try_emplace(asset_id); + if (!inserted) + return it->second.lock(); + std::shared_ptr ptr(new AssetType(std::move(library), asset_id), [&](AssetType* a) { + asset_map.erase(a->GetAssetId()); + m_total_bytes_loaded -= a->GetByteSizeInMemory(); + std::lock_guard lk(m_assets_lock); + m_assets_to_monitor.erase(a->GetAssetId()); + delete a; + }); + it->second = ptr; + m_asset_load_thread.Push(it->second); + return ptr; + } + + static constexpr auto TIME_BETWEEN_ASSET_MONITOR_CHECKS = std::chrono::milliseconds{500}; + std::map> m_textures; + std::map> m_game_textures; + std::thread m_asset_monitor_thread; + Common::Flag m_asset_monitor_thread_shutdown; + + std::size_t m_total_bytes_loaded = 0; + std::size_t m_max_memory_available = 0; + + std::map> m_assets_to_monitor; + std::mutex m_assets_lock; + Common::WorkQueueThread> m_asset_load_thread; +}; +} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/TextureAsset.cpp b/Source/Core/VideoCommon/Assets/TextureAsset.cpp new file mode 100644 index 0000000000..fd27ee80b6 --- /dev/null +++ b/Source/Core/VideoCommon/Assets/TextureAsset.cpp @@ -0,0 +1,77 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "VideoCommon/Assets/TextureAsset.h" + +#include "Common/Logging/Log.h" + +namespace VideoCommon +{ +CustomAssetLibrary::LoadInfo RawTextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id) +{ + std::lock_guard lk(m_lock); + const auto loaded_info = m_owning_library->LoadTexture(asset_id, &m_data); + if (loaded_info.m_bytes_loaded == 0) + return {}; + m_loaded = true; + return loaded_info; +} + +CustomAssetLibrary::LoadInfo GameTextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id) +{ + std::lock_guard lk(m_lock); + const auto loaded_info = m_owning_library->LoadGameTexture(asset_id, &m_data); + if (loaded_info.m_bytes_loaded == 0) + return {}; + m_loaded = true; + return loaded_info; +} + +bool GameTextureAsset::Validate(u32 native_width, u32 native_height) const +{ + std::lock_guard lk(m_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_levels.empty()) + { + ERROR_LOG_FMT(VIDEO, + "Game texture can't be validated for asset '{}' because no data was available.", + GetAssetId()); + return false; + } + + // Verify that the aspect ratio of the texture hasn't changed, as this could have + // side-effects. + const VideoCommon::CustomTextureData::Level& first_mip = m_data.m_levels[0]; + if (first_mip.width * native_height != first_mip.height * native_width) + { + ERROR_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); + return false; + } + + // 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)) + { + ERROR_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 false; + } + + return true; +} +} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/TextureAsset.h b/Source/Core/VideoCommon/Assets/TextureAsset.h new file mode 100644 index 0000000000..4cb0001a58 --- /dev/null +++ b/Source/Core/VideoCommon/Assets/TextureAsset.h @@ -0,0 +1,32 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "VideoCommon/Assets/CustomAsset.h" +#include "VideoCommon/Assets/CustomTextureData.h" + +namespace VideoCommon +{ +class RawTextureAsset final : public CustomLoadableAsset +{ +public: + using CustomLoadableAsset::CustomLoadableAsset; + +private: + CustomAssetLibrary::LoadInfo LoadImpl(const CustomAssetLibrary::AssetID& asset_id) override; +}; + +class GameTextureAsset 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 diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt index fa2811b410..ed8642fb1e 100644 --- a/Source/Core/VideoCommon/CMakeLists.txt +++ b/Source/Core/VideoCommon/CMakeLists.txt @@ -12,10 +12,14 @@ add_library(videocommon Assets/CustomAsset.h Assets/CustomAssetLibrary.cpp Assets/CustomAssetLibrary.h + Assets/CustomAssetLoader.cpp + Assets/CustomAssetLoader.h Assets/CustomTextureData.cpp Assets/CustomTextureData.h Assets/DirectFilesystemAssetLibrary.cpp Assets/DirectFilesystemAssetLibrary.h + Assets/TextureAsset.cpp + Assets/TextureAsset.h AsyncRequests.cpp AsyncRequests.h AsyncShaderCompiler.cpp