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

Co-authored-by: Jordan Woyak <jordan.woyak@gmail.com>
This commit is contained in:
iwubcode
2025-03-01 21:51:21 -06:00
parent d8ea31ca46
commit 70abcb2030
6 changed files with 689 additions and 0 deletions

View File

@ -0,0 +1,229 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "Common/Logging/Log.h"
#include "Common/MemoryUtil.h"
#include "UICommon/UICommon.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/VideoEvents.h"
namespace VideoCommon
{
void CustomResourceManager::Initialize()
{
// Use half of available system memory but leave at least 2GiB unused for system stability.
constexpr size_t must_keep_unused = 2 * size_t(1024 * 1024 * 1024);
const size_t sys_mem = Common::MemPhysical();
const size_t keep_unused_mem = std::max(sys_mem / 2, std::min(sys_mem, must_keep_unused));
m_max_ram_available = sys_mem - keep_unused_mem;
if (m_max_ram_available == 0)
ERROR_LOG_FMT(VIDEO, "Not enough system memory for custom resources.");
m_asset_loader.Initialize();
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_active_assets = {};
m_pending_assets = {};
m_asset_handle_to_data.clear();
m_asset_id_to_handle.clear();
m_texture_data_asset_cache.clear();
m_dirty_assets.clear();
m_ram_used = 0;
}
void CustomResourceManager::MarkAssetDirty(const CustomAssetLibrary::AssetID& asset_id)
{
std::lock_guard guard(m_dirty_mutex);
m_dirty_assets.insert(asset_id);
}
CustomResourceManager::TextureTimePair CustomResourceManager::GetTextureDataFromAsset(
const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library)
{
auto& resource = m_texture_data_asset_cache[asset_id];
if (resource.asset_data != nullptr &&
resource.asset_data->load_status == AssetData::LoadStatus::ResourceDataAvailable)
{
m_active_assets.MakeAssetHighestPriority(resource.asset->GetHandle(), resource.asset);
return {resource.texture_data, resource.asset->GetLastLoadedTime()};
}
// If there is an error, don't try and load again until the error is fixed
if (resource.asset_data != nullptr && resource.asset_data->has_load_error)
return {};
LoadTextureDataAsset(asset_id, std::move(library), &resource);
m_active_assets.MakeAssetHighestPriority(resource.asset->GetHandle(), resource.asset);
return {};
}
void CustomResourceManager::LoadTextureDataAsset(
const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library, InternalTextureDataResource* resource)
{
if (!resource->asset)
{
resource->asset =
CreateAsset<TextureAsset>(asset_id, AssetData::AssetType::TextureData, std::move(library));
resource->asset_data = &m_asset_handle_to_data[resource->asset->GetHandle()];
}
auto texture_data = resource->asset->GetData();
if (!texture_data || resource->asset_data->load_status == AssetData::LoadStatus::PendingReload)
{
// Tell the system we are still interested in loading this asset
const auto asset_handle = resource->asset->GetHandle();
m_pending_assets.MakeAssetHighestPriority(asset_handle,
m_asset_handle_to_data[asset_handle].asset.get());
}
else if (resource->asset_data->load_status == AssetData::LoadStatus::LoadFinished)
{
resource->texture_data = std::move(texture_data);
resource->asset_data->load_status = AssetData::LoadStatus::ResourceDataAvailable;
}
}
void CustomResourceManager::XFBTriggered()
{
ProcessDirtyAssets();
ProcessLoadedAssets();
if (m_ram_used > m_max_ram_available)
{
RemoveAssetsUntilBelowMemoryLimit();
}
if (m_pending_assets.IsEmpty())
return;
if (m_ram_used > m_max_ram_available)
return;
const u64 allowed_memory = m_max_ram_available - m_ram_used;
m_asset_loader.ScheduleAssetsToLoad(m_pending_assets.Elements(), allowed_memory);
}
void CustomResourceManager::ProcessDirtyAssets()
{
decltype(m_dirty_assets) dirty_assets;
if (const auto lk = std::unique_lock{m_dirty_mutex, std::try_to_lock})
std::swap(dirty_assets, m_dirty_assets);
const auto now = CustomAsset::ClockType::now();
for (const auto& asset_id : dirty_assets)
{
if (const auto it = m_asset_id_to_handle.find(asset_id); it != m_asset_id_to_handle.end())
{
const auto asset_handle = it->second;
AssetData& asset_data = m_asset_handle_to_data[asset_handle];
asset_data.load_status = AssetData::LoadStatus::PendingReload;
asset_data.load_request_time = now;
// Asset was reloaded, clear any errors we might have
asset_data.has_load_error = false;
m_pending_assets.InsertAsset(it->second, asset_data.asset.get());
DEBUG_LOG_FMT(VIDEO, "Dirty asset pending reload: {}", asset_data.asset->GetAssetId());
}
}
}
void CustomResourceManager::ProcessLoadedAssets()
{
const auto load_results = m_asset_loader.TakeLoadResults();
// Update the ram with the change in memory from the loader
//
// Note: Assets with outstanding reload requests will have
// two copies in memory temporarily (the old data stored in
// the asset shared_ptr that the resource manager owns, and
// the new data loaded from the loader in the asset's shared_ptr)
// This temporary duplication will not be reflected in the
// resource manager's ram used
m_ram_used += load_results.change_in_memory;
for (const auto& [handle, load_successful] : load_results.asset_handles)
{
AssetData& asset_data = m_asset_handle_to_data[handle];
// If we have a reload request that is newer than our loaded time
// we need to wait for another reload.
if (asset_data.load_request_time > asset_data.asset->GetLastLoadedTime())
continue;
m_pending_assets.RemoveAsset(handle);
asset_data.load_request_time = {};
if (!load_successful)
{
asset_data.has_load_error = true;
}
else
{
m_active_assets.InsertAsset(handle, asset_data.asset.get());
asset_data.load_status = AssetData::LoadStatus::LoadFinished;
}
}
}
void CustomResourceManager::RemoveAssetsUntilBelowMemoryLimit()
{
const u64 threshold_ram = m_max_ram_available * 8 / 10;
if (m_ram_used > threshold_ram)
{
INFO_LOG_FMT(VIDEO, "Memory usage over threshold: {}", UICommon::FormatSize(m_ram_used));
}
// Clear out least recently used resources until
// we get safely in our threshold
while (m_ram_used > threshold_ram && m_active_assets.Size() > 0)
{
auto* const asset = m_active_assets.RemoveLowestPriorityAsset();
AssetData& asset_data = m_asset_handle_to_data[asset->GetHandle()];
// Remove the resource manager's cached entry with its asset data
if (asset_data.type == AssetData::AssetType::TextureData)
{
m_texture_data_asset_cache.erase(asset->GetAssetId());
}
// Remove the asset's copy
const std::size_t bytes_unloaded = asset_data.asset->Unload();
m_ram_used -= bytes_unloaded;
asset_data.load_status = AssetData::LoadStatus::Unloaded;
asset_data.load_request_time = {};
INFO_LOG_FMT(VIDEO, "Unloading asset: {} ({})", asset_data.asset->GetAssetId(),
UICommon::FormatSize(bytes_unloaded));
}
}
} // namespace VideoCommon