VideoCommon: add resource manager and new asset loader; the resource manager uses a least recently used cache to determine which assets get priority for loading. Additionally, if the system is low on memory, assets will be purged with the less requested assets being the first to go. The loader is multithreaded now and loads assets as quickly as possible as long as memory is available

This commit is contained in:
iwubcode 2025-03-01 21:51:21 -06:00
parent 07b4c53371
commit 4489a30d4e
6 changed files with 632 additions and 0 deletions

View file

@ -659,6 +659,8 @@
<ClInclude Include="VideoCommon\Assets\CustomAsset.h" />
<ClInclude Include="VideoCommon\Assets\CustomAssetLibrary.h" />
<ClInclude Include="VideoCommon\Assets\CustomAssetLoader.h" />
<ClInclude Include="VideoCommon\Assets\CustomAssetLoader2.h" />
<ClInclude Include="VideoCommon\Assets\CustomResourceManager.h" />
<ClInclude Include="VideoCommon\Assets\CustomTextureData.h" />
<ClInclude Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.h" />
<ClInclude Include="VideoCommon\Assets\MaterialAsset.h" />
@ -1306,6 +1308,8 @@
<ClCompile Include="VideoCommon\Assets\CustomAsset.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomAssetLibrary.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomAssetLoader.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomAssetLoader2.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomResourceManager.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomTextureData.cpp" />
<ClCompile Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.cpp" />
<ClCompile Include="VideoCommon\Assets\MaterialAsset.cpp" />

View file

@ -0,0 +1,175 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/CustomAssetLoader2.h"
#include "Common/Logging/Log.h"
#include "Common/Thread.h"
namespace VideoCommon
{
void CustomAssetLoader2::Initialize()
{
ResizeWorkerThreads(2);
}
void CustomAssetLoader2 ::Shutdown()
{
Reset(false);
}
void CustomAssetLoader2::Reset(bool restart_worker_threads)
{
const std::size_t worker_thread_count = m_worker_threads.size();
StopWorkerThreads();
{
std::lock_guard<std::mutex> guard(m_pending_work_lock);
m_pending_assets.clear();
m_max_memory_allowed = 0;
m_current_asset_memory = 0;
}
{
std::lock_guard<std::mutex> guard(m_completed_work_lock);
m_completed_asset_session_ids.clear();
m_completed_asset_memory = 0;
}
if (restart_worker_threads)
{
StartWorkerThreads(static_cast<u32>(worker_thread_count));
}
}
bool CustomAssetLoader2::StartWorkerThreads(u32 num_worker_threads)
{
if (num_worker_threads == 0)
return true;
for (u32 i = 0; i < num_worker_threads; i++)
{
m_worker_thread_start_result.store(false);
void* thread_param = nullptr;
std::thread thr(&CustomAssetLoader2::WorkerThreadEntryPoint, this, thread_param);
m_init_event.Wait();
if (!m_worker_thread_start_result.load())
{
WARN_LOG_FMT(VIDEO, "Failed to start asset load worker thread.");
thr.join();
break;
}
m_worker_threads.push_back(std::move(thr));
}
return HasWorkerThreads();
}
bool CustomAssetLoader2::ResizeWorkerThreads(u32 num_worker_threads)
{
if (m_worker_threads.size() == num_worker_threads)
return true;
StopWorkerThreads();
return StartWorkerThreads(num_worker_threads);
}
bool CustomAssetLoader2::HasWorkerThreads() const
{
return !m_worker_threads.empty();
}
void CustomAssetLoader2::StopWorkerThreads()
{
if (!HasWorkerThreads())
return;
// Signal worker threads to stop, and wake all of them.
{
std::lock_guard<std::mutex> guard(m_pending_work_lock);
m_exit_flag.Set();
m_worker_thread_wake.notify_all();
}
// Wait for worker threads to exit.
for (std::thread& thr : m_worker_threads)
thr.join();
m_worker_threads.clear();
m_exit_flag.Clear();
}
void CustomAssetLoader2::WorkerThreadEntryPoint(void* param)
{
Common::SetCurrentThreadName("Asset Loader Worker");
m_worker_thread_start_result.store(true);
m_init_event.Set();
WorkerThreadRun();
}
void CustomAssetLoader2::WorkerThreadRun()
{
std::unique_lock<std::mutex> pending_lock(m_pending_work_lock);
while (!m_exit_flag.IsSet())
{
m_worker_thread_wake.wait(pending_lock);
while (!m_pending_assets.empty() && !m_exit_flag.IsSet())
{
auto pending_iter = m_pending_assets.begin();
const auto item = *pending_iter;
m_pending_assets.erase(pending_iter);
if ((m_current_asset_memory + m_completed_asset_memory) > m_max_memory_allowed)
break;
pending_lock.unlock();
if (item->Load())
{
std::lock_guard<std::mutex> completed_guard(m_completed_work_lock);
m_completed_asset_memory += item->GetByteSizeInMemory();
m_completed_asset_session_ids.push_back(item->GetSessionId());
}
pending_lock.lock();
}
}
}
std::vector<std::size_t>
CustomAssetLoader2::LoadAssets(const std::list<CustomAsset*>& pending_assets,
u64 current_loaded_memory, u64 max_memory_allowed)
{
u64 total_memory = current_loaded_memory;
std::vector<std::size_t> completed_asset_session_ids;
{
std::lock_guard<std::mutex> guard(m_completed_work_lock);
m_completed_asset_session_ids.swap(completed_asset_session_ids);
total_memory += m_completed_asset_memory;
m_completed_asset_memory = 0;
}
if (pending_assets.empty())
return completed_asset_session_ids;
if (total_memory > max_memory_allowed)
return completed_asset_session_ids;
// There's new assets to process, notify worker threads
{
std::lock_guard<std::mutex> guard(m_pending_work_lock);
m_pending_assets = pending_assets;
m_current_asset_memory = total_memory;
m_max_memory_allowed = max_memory_allowed;
if (m_current_asset_memory < m_max_memory_allowed)
m_worker_thread_wake.notify_all();
}
return completed_asset_session_ids;
}
} // namespace VideoCommon

View file

@ -0,0 +1,65 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <condition_variable>
#include <list>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include "Common/Event.h"
#include "Common/Flag.h"
#include "VideoCommon/Assets/CustomAsset.h"
namespace VideoCommon
{
class CustomAssetLoader2
{
public:
CustomAssetLoader2() = default;
~CustomAssetLoader2() = default;
CustomAssetLoader2(const CustomAssetLoader2&) = delete;
CustomAssetLoader2(CustomAssetLoader2&&) = delete;
CustomAssetLoader2& operator=(const CustomAssetLoader2&) = delete;
CustomAssetLoader2& operator=(CustomAssetLoader2&&) = delete;
void Initialize();
void Shutdown();
// Returns a vector of asset session ids that were loaded in the last frame
std::vector<std::size_t> LoadAssets(const std::list<CustomAsset*>& pending_assets,
u64 current_loaded_memory, u64 max_memory_allowed);
void Reset(bool restart_worker_threads = true);
private:
bool StartWorkerThreads(u32 num_worker_threads);
bool ResizeWorkerThreads(u32 num_worker_threads);
bool HasWorkerThreads() const;
void StopWorkerThreads();
void WorkerThreadEntryPoint(void* param);
void WorkerThreadRun();
Common::Flag m_exit_flag;
Common::Event m_init_event;
std::vector<std::thread> m_worker_threads;
std::atomic_bool m_worker_thread_start_result{false};
std::list<CustomAsset*> m_pending_assets;
std::atomic<u64> m_current_asset_memory = 0;
u64 m_max_memory_allowed = 0;
std::mutex m_pending_work_lock;
std::condition_variable m_worker_thread_wake;
std::vector<std::size_t> m_completed_asset_session_ids;
std::atomic<u64> m_completed_asset_memory = 0;
std::mutex m_completed_work_lock;
};
} // namespace VideoCommon

View file

@ -0,0 +1,202 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/CustomResourceManager.h"
#include <fmt/format.h>
#include "Common/MathUtil.h"
#include "Common/MemoryUtil.h"
#include "Common/VariantUtil.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/BPMemory.h"
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/VideoEvents.h"
namespace VideoCommon
{
void CustomResourceManager::Initialize()
{
m_asset_loader.Initialize();
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_ram_available =
(sys_mem / 2 < recommended_min_mem) ? (sys_mem / 2) : (sys_mem - recommended_min_mem);
m_xfb_event = AfterFrameEvent::Register([this](Core::System&) { XFBTriggered(""); },
"CustomResourceManager");
}
void CustomResourceManager::Shutdown()
{
Reset();
m_asset_loader.Shutdown();
}
void CustomResourceManager::Reset()
{
m_asset_loader.Reset(true);
m_loaded_assets = {};
m_pending_assets = {};
m_session_id_to_asset_data.clear();
m_asset_id_to_session_id.clear();
m_ram_used = 0;
}
void CustomResourceManager::ReloadAsset(const CustomAssetLibrary::AssetID& asset_id)
{
std::lock_guard<std::mutex> guard(m_reload_mutex);
m_assets_to_reload.insert(asset_id);
}
CustomTextureData* CustomResourceManager::GetTextureDataFromAsset(
const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library)
{
const auto [it, inserted] =
m_texture_data_asset_cache.try_emplace(asset_id, InternalTextureDataResource{});
if (it->second.asset_data &&
it->second.asset_data->load_type == AssetData::LoadType::LoadFinalyzed)
{
m_loaded_assets.put(it->second.asset->GetSessionId(), it->second.asset);
return &it->second.texture_data->m_texture;
}
LoadTextureDataAsset(asset_id, std::move(library), &it->second);
return nullptr;
}
void CustomResourceManager::LoadTextureDataAsset(
const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
InternalTextureDataResource* internal_texture_data)
{
if (!internal_texture_data->asset)
{
internal_texture_data->asset =
CreateAsset<GameTextureAsset>(asset_id, AssetData::AssetType::TextureData, library);
internal_texture_data->asset_data =
&m_session_id_to_asset_data[internal_texture_data->asset->GetSessionId()];
}
auto texture_data = internal_texture_data->asset->GetData();
if (!texture_data ||
internal_texture_data->asset_data->load_type == AssetData::LoadType::PendingReload)
{
// Tell the system we are still interested in loading this asset
const auto session_id = internal_texture_data->asset->GetSessionId();
m_pending_assets.put(session_id, m_session_id_to_asset_data[session_id].asset.get());
}
else if (internal_texture_data->asset_data->load_type == AssetData::LoadType::LoadFinished)
{
internal_texture_data->texture_data = std::move(texture_data);
internal_texture_data->asset_data->load_type = AssetData::LoadType::LoadFinalyzed;
}
}
void CustomResourceManager::XFBTriggered(std::string_view)
{
std::set<std::size_t> session_ids_reloaded_this_frame;
// Look for any assets requested to be reloaded
{
decltype(m_assets_to_reload) assets_to_reload;
if (m_reload_mutex.try_lock())
{
std::swap(assets_to_reload, m_assets_to_reload);
m_reload_mutex.unlock();
}
for (const auto& asset_id : assets_to_reload)
{
if (const auto it = m_asset_id_to_session_id.find(asset_id);
it != m_asset_id_to_session_id.end())
{
const auto session_id = it->second;
session_ids_reloaded_this_frame.insert(session_id);
AssetData& asset_data = m_session_id_to_asset_data[session_id];
asset_data.load_type = AssetData::LoadType::PendingReload;
asset_data.has_errors = false;
for (const auto owner_session_id : asset_data.asset_owners)
{
AssetData& owner_asset_data = m_session_id_to_asset_data[owner_session_id];
if (owner_asset_data.load_type == AssetData::LoadType::LoadFinalyzed)
{
owner_asset_data.load_type = AssetData::LoadType::DependenciesChanged;
}
}
m_pending_assets.put(it->second, asset_data.asset.get());
}
}
}
if (m_ram_used > m_max_ram_available)
{
const u64 threshold_ram = 0.8f * m_max_ram_available;
u64 ram_used = m_ram_used;
// Clear out least recently used resources until
// we get safely in our threshold
while (ram_used > threshold_ram && m_loaded_assets.size() > 0)
{
const auto asset = m_loaded_assets.pop();
ram_used -= asset->GetByteSizeInMemory();
AssetData& asset_data = m_session_id_to_asset_data[asset->GetSessionId()];
if (asset_data.type == AssetData::AssetType::TextureData)
{
m_texture_data_asset_cache.erase(asset->GetAssetId());
}
asset_data.asset.reset();
asset_data.load_type = AssetData::LoadType::Unloaded;
}
// Recalculate to ensure accuracy
m_ram_used = 0;
for (const auto asset : m_loaded_assets.elements())
{
m_ram_used += asset->GetByteSizeInMemory();
}
}
if (m_pending_assets.empty())
return;
const auto asset_session_ids_loaded =
m_asset_loader.LoadAssets(m_pending_assets.elements(), m_ram_used, m_max_ram_available);
for (const std::size_t session_id : asset_session_ids_loaded)
{
// While unlikely, if we loaded an asset in the previous frame but it was reloaded
// this frame, we should ignore this load and wait on the reload
if (session_ids_reloaded_this_frame.count(session_id) > 0) [[unlikely]]
continue;
m_pending_assets.erase(session_id);
AssetData& asset_data = m_session_id_to_asset_data[session_id];
m_loaded_assets.put(session_id, asset_data.asset.get());
asset_data.load_type = AssetData::LoadType::LoadFinished;
m_ram_used += asset_data.asset->GetByteSizeInMemory();
for (const auto owner_session_id : asset_data.asset_owners)
{
AssetData& owner_asset_data = m_session_id_to_asset_data[owner_session_id];
if (owner_asset_data.load_type == AssetData::LoadType::LoadFinalyzed)
{
owner_asset_data.load_type = AssetData::LoadType::DependenciesChanged;
}
}
}
}
} // namespace VideoCommon

View file

@ -0,0 +1,182 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
#include "Common/CommonTypes.h"
#include "Common/HookableEvent.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include "VideoCommon/Assets/CustomAssetLoader2.h"
#include "VideoCommon/Assets/CustomTextureData.h"
namespace VideoCommon
{
class GameTextureAsset;
class CustomResourceManager
{
public:
void Initialize();
void Shutdown();
void Reset();
// Requests that an asset that exists be reloaded
void ReloadAsset(const CustomAssetLibrary::AssetID& asset_id);
void XFBTriggered(std::string_view texture_hash);
CustomTextureData*
GetTextureDataFromAsset(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library);
private:
struct AssetData
{
std::unique_ptr<CustomAsset> asset;
CustomAssetLibrary::TimeType load_request_time = {};
std::set<std::size_t> asset_owners;
enum class AssetType
{
TextureData
};
AssetType type;
enum class LoadType
{
PendingReload,
LoadFinished,
LoadFinalyzed,
DependenciesChanged,
Unloaded
};
LoadType load_type = LoadType::PendingReload;
bool has_errors = false;
};
struct InternalTextureDataResource
{
AssetData* asset_data = nullptr;
VideoCommon::GameTextureAsset* asset = nullptr;
std::shared_ptr<TextureData> texture_data;
};
void LoadTextureDataAsset(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
InternalTextureDataResource* internal_texture_data);
template <typename T>
T* CreateAsset(const CustomAssetLibrary::AssetID& asset_id, AssetData::AssetType asset_type,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library)
{
const auto [it, added] =
m_asset_id_to_session_id.try_emplace(asset_id, m_session_id_to_asset_data.size());
if (added)
{
AssetData asset_data;
asset_data.asset = std::make_unique<T>(library, asset_id, it->second);
asset_data.type = asset_type;
asset_data.has_errors = false;
asset_data.load_type = AssetData::LoadType::PendingReload;
asset_data.load_request_time = {};
m_session_id_to_asset_data.insert_or_assign(it->second, std::move(asset_data));
// Synchronize the priority cache session id
m_pending_assets.prepare();
m_loaded_assets.prepare();
}
auto& asset_data_from_session = m_session_id_to_asset_data[it->second];
// Asset got unloaded, rebuild it with the same metadata
if (!asset_data_from_session.asset)
{
asset_data_from_session.asset = std::make_unique<T>(library, asset_id, it->second);
asset_data_from_session.has_errors = false;
asset_data_from_session.load_type = AssetData::LoadType::PendingReload;
}
return static_cast<T*>(asset_data_from_session.asset.get());
}
class LeastRecentlyUsedCache
{
public:
const std::list<CustomAsset*>& elements() const { return m_asset_cache; }
void put(u64 asset_session_id, CustomAsset* asset)
{
erase(asset_session_id);
m_asset_cache.push_front(asset);
m_iterator_lookup[m_asset_cache.front()->GetSessionId()] = m_asset_cache.begin();
}
CustomAsset* pop()
{
if (m_asset_cache.empty()) [[unlikely]]
return nullptr;
const auto ret = m_asset_cache.back();
if (ret != nullptr)
{
m_iterator_lookup[ret->GetSessionId()].reset();
}
m_asset_cache.pop_back();
return ret;
}
void prepare() { m_iterator_lookup.push_back(std::nullopt); }
void erase(u64 asset_session_id)
{
if (const auto iter = m_iterator_lookup[asset_session_id])
{
m_asset_cache.erase(*iter);
m_iterator_lookup[asset_session_id].reset();
}
}
bool empty() const { return m_asset_cache.empty(); }
std::size_t size() const { return m_asset_cache.size(); }
private:
std::list<CustomAsset*> m_asset_cache;
// Note: this vector is expected to be kept in sync with
// the total amount of (unique) assets ever seen
std::vector<std::optional<decltype(m_asset_cache)::iterator>> m_iterator_lookup;
};
LeastRecentlyUsedCache m_loaded_assets;
LeastRecentlyUsedCache m_pending_assets;
std::map<std::size_t, AssetData> m_session_id_to_asset_data;
std::map<CustomAssetLibrary::AssetID, std::size_t> m_asset_id_to_session_id;
u64 m_ram_used = 0;
u64 m_max_ram_available = 0;
std::map<CustomAssetLibrary::AssetID, InternalTextureDataResource> m_texture_data_asset_cache;
std::mutex m_reload_mutex;
std::set<CustomAssetLibrary::AssetID> m_assets_to_reload;
CustomAssetLoader2 m_asset_loader;
Common::EventHook m_xfb_event;
};
} // namespace VideoCommon

View file

@ -14,6 +14,10 @@ add_library(videocommon
Assets/CustomAssetLibrary.h
Assets/CustomAssetLoader.cpp
Assets/CustomAssetLoader.h
Assets/CustomAssetLoader2.cpp
Assets/CustomAssetLoader2.h
Assets/CustomResourceManager.cpp
Assets/CustomResourceManager.h
Assets/CustomTextureData.cpp
Assets/CustomTextureData.h
Assets/DirectFilesystemAssetLibrary.cpp